os_macos.go raw
1 // SPDX-License-Identifier: Unlicense OR MIT
2
3 // +build darwin,!ios
4
5 package wm
6
7 import (
8 "errors"
9 "image"
10 "runtime"
11 "time"
12 "unicode"
13 "unicode/utf16"
14 "unsafe"
15
16 "github.com/p9c/p9/pkg/gel/gio/f32"
17 "github.com/p9c/p9/pkg/gel/gio/io/clipboard"
18 "github.com/p9c/p9/pkg/gel/gio/io/key"
19 "github.com/p9c/p9/pkg/gel/gio/io/pointer"
20 "github.com/p9c/p9/pkg/gel/gio/io/system"
21 "github.com/p9c/p9/pkg/gel/gio/unit"
22
23 _ "github.com/p9c/p9/pkg/gel/gio/internal/cocoainit"
24 )
25
26 /*
27 #cgo CFLAGS: -DGL_SILENCE_DEPRECATION -Werror -Wno-deprecated-declarations -fmodules -fobjc-arc -x objective-c
28
29 #include <AppKit/AppKit.h>
30
31 #define GIO_MOUSE_MOVE 1
32 #define GIO_MOUSE_UP 2
33 #define GIO_MOUSE_DOWN 3
34 #define GIO_MOUSE_SCROLL 4
35
36 __attribute__ ((visibility ("hidden"))) void gio_main(void);
37 __attribute__ ((visibility ("hidden"))) CGFloat gio_viewWidth(CFTypeRef viewRef);
38 __attribute__ ((visibility ("hidden"))) CGFloat gio_viewHeight(CFTypeRef viewRef);
39 __attribute__ ((visibility ("hidden"))) CGFloat gio_getViewBackingScale(CFTypeRef viewRef);
40 __attribute__ ((visibility ("hidden"))) CGFloat gio_getScreenBackingScale(void);
41 __attribute__ ((visibility ("hidden"))) CFTypeRef gio_readClipboard(void);
42 __attribute__ ((visibility ("hidden"))) void gio_writeClipboard(unichar *chars, NSUInteger length);
43 __attribute__ ((visibility ("hidden"))) void gio_setNeedsDisplay(CFTypeRef viewRef);
44 __attribute__ ((visibility ("hidden"))) void gio_toggleFullScreen(CFTypeRef windowRef);
45 __attribute__ ((visibility ("hidden"))) CFTypeRef gio_createWindow(CFTypeRef viewRef, const char *title, CGFloat width, CGFloat height, CGFloat minWidth, CGFloat minHeight, CGFloat maxWidth, CGFloat maxHeight);
46 __attribute__ ((visibility ("hidden"))) void gio_makeKeyAndOrderFront(CFTypeRef windowRef);
47 __attribute__ ((visibility ("hidden"))) NSPoint gio_cascadeTopLeftFromPoint(CFTypeRef windowRef, NSPoint topLeft);
48 __attribute__ ((visibility ("hidden"))) void gio_close(CFTypeRef windowRef);
49 __attribute__ ((visibility ("hidden"))) void gio_setSize(CFTypeRef windowRef, CGFloat width, CGFloat height);
50 __attribute__ ((visibility ("hidden"))) void gio_setMinSize(CFTypeRef windowRef, CGFloat width, CGFloat height);
51 __attribute__ ((visibility ("hidden"))) void gio_setMaxSize(CFTypeRef windowRef, CGFloat width, CGFloat height);
52 __attribute__ ((visibility ("hidden"))) void gio_setTitle(CFTypeRef windowRef, const char *title);
53 */
54 import "C"
55
56 func init() {
57 // Darwin requires that UI operations happen on the main thread only.
58 runtime.LockOSThread()
59 }
60
61 type window struct {
62 view C.CFTypeRef
63 window C.CFTypeRef
64 w Callbacks
65 stage system.Stage
66 displayLink *displayLink
67 cursor pointer.CursorName
68
69 scale float32
70 mode WindowMode
71 }
72
73 // viewMap is the mapping from Cocoa NSViews to Go windows.
74 var viewMap = make(map[C.CFTypeRef]*window)
75
76 var viewFactory func() C.CFTypeRef
77
78 // launched is closed when applicationDidFinishLaunching is called.
79 var launched = make(chan struct{})
80
81 // nextTopLeft is the offset to use for the next window's call to
82 // cascadeTopLeftFromPoint.
83 var nextTopLeft C.NSPoint
84
85 // mustView is like lookupView, except that it panics
86 // if the view isn't mapped.
87 func mustView(view C.CFTypeRef) *window {
88 w, ok := lookupView(view)
89 if !ok {
90 panic("no window for view")
91 }
92 return w
93 }
94
95 func lookupView(view C.CFTypeRef) (*window, bool) {
96 w, exists := viewMap[view]
97 if !exists {
98 return nil, false
99 }
100 return w, true
101 }
102
103 func deleteView(view C.CFTypeRef) {
104 delete(viewMap, view)
105 }
106
107 func insertView(view C.CFTypeRef, w *window) {
108 viewMap[view] = w
109 }
110
111 func (w *window) contextView() C.CFTypeRef {
112 return w.view
113 }
114
115 func (w *window) ReadClipboard() {
116 runOnMain(func() {
117 content := nsstringToString(C.gio_readClipboard())
118 w.w.Event(clipboard.Event{Text: content})
119 })
120 }
121
122 func (w *window) WriteClipboard(s string) {
123 u16 := utf16.Encode([]rune(s))
124 runOnMain(func() {
125 var chars *C.unichar
126 if len(u16) > 0 {
127 chars = (*C.unichar)(unsafe.Pointer(&u16[0]))
128 }
129 C.gio_writeClipboard(chars, C.NSUInteger(len(u16)))
130 })
131 }
132
133 func (w *window) Option(opts *Options) {
134 w.runOnMain(func() {
135 screenScale := float32(C.gio_getScreenBackingScale())
136 cfg := configFor(screenScale)
137 val := func(v unit.Value) float32 {
138 return float32(cfg.Px(v)) / screenScale
139 }
140 if o := opts.Size; o != nil {
141 width := val(o.Width)
142 height := val(o.Height)
143 if width > 0 || height > 0 {
144 C.gio_setSize(w.window, C.CGFloat(width), C.CGFloat(height))
145 }
146 }
147 if o := opts.MinSize; o != nil {
148 width := val(o.Width)
149 height := val(o.Height)
150 if width > 0 || height > 0 {
151 C.gio_setMinSize(w.window, C.CGFloat(width), C.CGFloat(height))
152 }
153 }
154 if o := opts.MaxSize; o != nil {
155 width := val(o.Width)
156 height := val(o.Height)
157 if width > 0 || height > 0 {
158 C.gio_setMaxSize(w.window, C.CGFloat(width), C.CGFloat(height))
159 }
160 }
161 if o := opts.Title; o != nil {
162 title := C.CString(*o)
163 defer C.free(unsafe.Pointer(title))
164 C.gio_setTitle(w.window, title)
165 }
166 if o := opts.WindowMode; o != nil {
167 w.SetWindowMode(*o)
168 }
169 })
170 }
171
172 func (w *window) SetWindowMode(mode WindowMode) {
173 switch mode {
174 case w.mode:
175 case Windowed, Fullscreen:
176 C.gio_toggleFullScreen(w.window)
177 w.mode = mode
178 }
179 }
180
181 func (w *window) SetCursor(name pointer.CursorName) {
182 w.cursor = windowSetCursor(w.cursor, name)
183 }
184
185 func (w *window) ShowTextInput(show bool) {}
186
187 func (w *window) SetAnimating(anim bool) {
188 if anim {
189 w.displayLink.Start()
190 } else {
191 w.displayLink.Stop()
192 }
193 }
194
195 func (w *window) runOnMain(f func()) {
196 runOnMain(func() {
197 // Make sure the view is still valid. The window might've been closed
198 // during the switch to the main thread.
199 if w.view != 0 {
200 f()
201 }
202 })
203 }
204
205 func (w *window) Close() {
206 w.runOnMain(func() {
207 C.gio_close(w.window)
208 })
209 }
210
211 func (w *window) setStage(stage system.Stage) {
212 if stage == w.stage {
213 return
214 }
215 w.stage = stage
216 w.w.Event(system.StageEvent{Stage: stage})
217 }
218
219 //export gio_onKeys
220 func gio_onKeys(view C.CFTypeRef, cstr *C.char, ti C.double, mods C.NSUInteger, keyDown C.bool) {
221 str := C.GoString(cstr)
222 kmods := convertMods(mods)
223 ks := key.Release
224 if keyDown {
225 ks = key.Press
226 }
227 w := mustView(view)
228 for _, k := range str {
229 if n, ok := convertKey(k); ok {
230 w.w.Event(key.Event{
231 Name: n,
232 Modifiers: kmods,
233 State: ks,
234 })
235 }
236 }
237 }
238
239 //export gio_onText
240 func gio_onText(view C.CFTypeRef, cstr *C.char) {
241 str := C.GoString(cstr)
242 w := mustView(view)
243 w.w.Event(key.EditEvent{Text: str})
244 }
245
246 //export gio_onMouse
247 func gio_onMouse(view C.CFTypeRef, cdir C.int, cbtns C.NSUInteger, x, y, dx, dy C.CGFloat, ti C.double, mods C.NSUInteger) {
248 var typ pointer.Type
249 switch cdir {
250 case C.GIO_MOUSE_MOVE:
251 typ = pointer.Move
252 case C.GIO_MOUSE_UP:
253 typ = pointer.Release
254 case C.GIO_MOUSE_DOWN:
255 typ = pointer.Press
256 case C.GIO_MOUSE_SCROLL:
257 typ = pointer.Scroll
258 default:
259 panic("invalid direction")
260 }
261 var btns pointer.Buttons
262 if cbtns&(1<<0) != 0 {
263 btns |= pointer.ButtonPrimary
264 }
265 if cbtns&(1<<1) != 0 {
266 btns |= pointer.ButtonSecondary
267 }
268 if cbtns&(1<<2) != 0 {
269 btns |= pointer.ButtonTertiary
270 }
271 t := time.Duration(float64(ti)*float64(time.Second) + .5)
272 w := mustView(view)
273 xf, yf := float32(x)*w.scale, float32(y)*w.scale
274 dxf, dyf := float32(dx)*w.scale, float32(dy)*w.scale
275 w.w.Event(pointer.Event{
276 Type: typ,
277 Source: pointer.Mouse,
278 Time: t,
279 Buttons: btns,
280 Position: f32.Point{X: xf, Y: yf},
281 Scroll: f32.Point{X: dxf, Y: dyf},
282 Modifiers: convertMods(mods),
283 })
284 }
285
286 //export gio_onDraw
287 func gio_onDraw(view C.CFTypeRef) {
288 w := mustView(view)
289 w.draw()
290 }
291
292 //export gio_onFocus
293 func gio_onFocus(view C.CFTypeRef, focus C.int) {
294 w := mustView(view)
295 w.w.Event(key.FocusEvent{Focus: focus == 1})
296 w.SetCursor(w.cursor)
297 }
298
299 //export gio_onChangeScreen
300 func gio_onChangeScreen(view C.CFTypeRef, did uint64) {
301 w := mustView(view)
302 w.displayLink.SetDisplayID(did)
303 }
304
305 func (w *window) draw() {
306 w.scale = float32(C.gio_getViewBackingScale(w.view))
307 wf, hf := float32(C.gio_viewWidth(w.view)), float32(C.gio_viewHeight(w.view))
308 if wf == 0 || hf == 0 {
309 return
310 }
311 width := int(wf*w.scale + .5)
312 height := int(hf*w.scale + .5)
313 cfg := configFor(w.scale)
314 w.setStage(system.StageRunning)
315 w.w.Event(FrameEvent{
316 FrameEvent: system.FrameEvent{
317 Now: time.Now(),
318 Size: image.Point{
319 X: width,
320 Y: height,
321 },
322 Metric: cfg,
323 },
324 Sync: true,
325 })
326 }
327
328 func configFor(scale float32) unit.Metric {
329 return unit.Metric{
330 PxPerDp: scale,
331 PxPerSp: scale,
332 }
333 }
334
335 //export gio_onClose
336 func gio_onClose(view C.CFTypeRef) {
337 w := mustView(view)
338 w.displayLink.Close()
339 deleteView(view)
340 w.w.Event(system.DestroyEvent{})
341 C.CFRelease(w.view)
342 w.view = 0
343 C.CFRelease(w.window)
344 w.window = 0
345 }
346
347 //export gio_onHide
348 func gio_onHide(view C.CFTypeRef) {
349 w := mustView(view)
350 w.setStage(system.StagePaused)
351 }
352
353 //export gio_onShow
354 func gio_onShow(view C.CFTypeRef) {
355 w := mustView(view)
356 w.setStage(system.StageRunning)
357 }
358
359 //export gio_onAppHide
360 func gio_onAppHide() {
361 for _, w := range viewMap {
362 w.setStage(system.StagePaused)
363 }
364 }
365
366 //export gio_onAppShow
367 func gio_onAppShow() {
368 for _, w := range viewMap {
369 w.setStage(system.StageRunning)
370 }
371 }
372
373 //export gio_onFinishLaunching
374 func gio_onFinishLaunching() {
375 close(launched)
376 }
377
378 func NewWindow(win Callbacks, opts *Options) error {
379 <-launched
380 errch := make(chan error)
381 runOnMain(func() {
382 w, err := newWindow(opts)
383 if err != nil {
384 errch <- err
385 return
386 }
387 errch <- nil
388 win.SetDriver(w)
389 w.w = win
390 w.window = C.gio_createWindow(w.view, nil, 0, 0, 0, 0, 0, 0)
391 w.Option(opts)
392 if nextTopLeft.x == 0 && nextTopLeft.y == 0 {
393 // cascadeTopLeftFromPoint treats (0, 0) as a no-op,
394 // and just returns the offset we need for the first window.
395 nextTopLeft = C.gio_cascadeTopLeftFromPoint(w.window, nextTopLeft)
396 }
397 nextTopLeft = C.gio_cascadeTopLeftFromPoint(w.window, nextTopLeft)
398 C.gio_makeKeyAndOrderFront(w.window)
399 })
400 return <-errch
401 }
402
403 func newWindow(opts *Options) (*window, error) {
404 view := viewFactory()
405 if view == 0 {
406 return nil, errors.New("CreateWindow: failed to create view")
407 }
408 scale := float32(C.gio_getViewBackingScale(view))
409 w := &window{
410 view: view,
411 scale: scale,
412 }
413 dl, err := NewDisplayLink(func() {
414 w.runOnMain(func() {
415 C.gio_setNeedsDisplay(w.view)
416 })
417 })
418 w.displayLink = dl
419 if err != nil {
420 C.CFRelease(view)
421 return nil, err
422 }
423 insertView(view, w)
424 return w, nil
425 }
426
427 func Main() {
428 C.gio_main()
429 }
430
431 func convertKey(k rune) (string, bool) {
432 var n string
433 switch k {
434 case 0x1b:
435 n = key.NameEscape
436 case C.NSLeftArrowFunctionKey:
437 n = key.NameLeftArrow
438 case C.NSRightArrowFunctionKey:
439 n = key.NameRightArrow
440 case C.NSUpArrowFunctionKey:
441 n = key.NameUpArrow
442 case C.NSDownArrowFunctionKey:
443 n = key.NameDownArrow
444 case 0xd:
445 n = key.NameReturn
446 case 0x3:
447 n = key.NameEnter
448 case C.NSHomeFunctionKey:
449 n = key.NameHome
450 case C.NSEndFunctionKey:
451 n = key.NameEnd
452 case 0x7f:
453 n = key.NameDeleteBackward
454 case C.NSDeleteFunctionKey:
455 n = key.NameDeleteForward
456 case C.NSPageUpFunctionKey:
457 n = key.NamePageUp
458 case C.NSPageDownFunctionKey:
459 n = key.NamePageDown
460 case C.NSF1FunctionKey:
461 n = "F1"
462 case C.NSF2FunctionKey:
463 n = "F2"
464 case C.NSF3FunctionKey:
465 n = "F3"
466 case C.NSF4FunctionKey:
467 n = "F4"
468 case C.NSF5FunctionKey:
469 n = "F5"
470 case C.NSF6FunctionKey:
471 n = "F6"
472 case C.NSF7FunctionKey:
473 n = "F7"
474 case C.NSF8FunctionKey:
475 n = "F8"
476 case C.NSF9FunctionKey:
477 n = "F9"
478 case C.NSF10FunctionKey:
479 n = "F10"
480 case C.NSF11FunctionKey:
481 n = "F11"
482 case C.NSF12FunctionKey:
483 n = "F12"
484 case 0x09, 0x19:
485 n = key.NameTab
486 case 0x20:
487 n = key.NameSpace
488 default:
489 k = unicode.ToUpper(k)
490 if !unicode.IsPrint(k) {
491 return "", false
492 }
493 n = string(k)
494 }
495 return n, true
496 }
497
498 func convertMods(mods C.NSUInteger) key.Modifiers {
499 var kmods key.Modifiers
500 if mods&C.NSAlternateKeyMask != 0 {
501 kmods |= key.ModAlt
502 }
503 if mods&C.NSControlKeyMask != 0 {
504 kmods |= key.ModCtrl
505 }
506 if mods&C.NSCommandKeyMask != 0 {
507 kmods |= key.ModCommand
508 }
509 if mods&C.NSShiftKeyMask != 0 {
510 kmods |= key.ModShift
511 }
512 return kmods
513 }
514