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