github.com/cybriq/giocore@v0.0.7-0.20210703034601-cfb9cb5f3900/app/internal/wm/os_macos.go (about)

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