github.com/utopiagio/gio@v0.0.8/app/os_darwin.go (about)

     1  // SPDX-License-Identifier: Unlicense OR MIT
     2  
     3  package app
     4  
     5  /*
     6  #include <Foundation/Foundation.h>
     7  
     8  __attribute__ ((visibility ("hidden"))) void gio_wakeupMainThread(void);
     9  __attribute__ ((visibility ("hidden"))) CFTypeRef gio_createDisplayLink(void);
    10  __attribute__ ((visibility ("hidden"))) void gio_releaseDisplayLink(CFTypeRef dl);
    11  __attribute__ ((visibility ("hidden"))) int gio_startDisplayLink(CFTypeRef dl);
    12  __attribute__ ((visibility ("hidden"))) int gio_stopDisplayLink(CFTypeRef dl);
    13  __attribute__ ((visibility ("hidden"))) void gio_setDisplayLinkDisplay(CFTypeRef dl, uint64_t did);
    14  __attribute__ ((visibility ("hidden"))) void gio_hideCursor();
    15  __attribute__ ((visibility ("hidden"))) void gio_showCursor();
    16  __attribute__ ((visibility ("hidden"))) void gio_setCursor(NSUInteger curID);
    17  
    18  static bool isMainThread() {
    19  	return [NSThread isMainThread];
    20  }
    21  
    22  static NSUInteger nsstringLength(CFTypeRef cstr) {
    23  	NSString *str = (__bridge NSString *)cstr;
    24  	return [str length];
    25  }
    26  
    27  static void nsstringGetCharacters(CFTypeRef cstr, unichar *chars, NSUInteger loc, NSUInteger length) {
    28  	NSString *str = (__bridge NSString *)cstr;
    29  	[str getCharacters:chars range:NSMakeRange(loc, length)];
    30  }
    31  
    32  static CFTypeRef newNSString(unichar *chars, NSUInteger length) {
    33  	@autoreleasepool {
    34  		NSString *s = [NSString string];
    35  		if (length > 0) {
    36  			s = [NSString stringWithCharacters:chars length:length];
    37  		}
    38  		return CFBridgingRetain(s);
    39  	}
    40  }
    41  */
    42  import "C"
    43  import (
    44  	"errors"
    45  	"sync"
    46  	"sync/atomic"
    47  	"time"
    48  	"unicode/utf16"
    49  	"unsafe"
    50  
    51  	"github.com/utopiagio/gio/io/pointer"
    52  )
    53  
    54  // displayLink is the state for a display link (CVDisplayLinkRef on macOS,
    55  // CADisplayLink on iOS). It runs a state-machine goroutine that keeps the
    56  // display link running for a while after being stopped to avoid the thread
    57  // start/stop overhead and because the CVDisplayLink sometimes fails to
    58  // start, stop and start again within a short duration.
    59  type displayLink struct {
    60  	callback func()
    61  	// states is for starting or stopping the display link.
    62  	states chan bool
    63  	// done is closed when the display link is destroyed.
    64  	done chan struct{}
    65  	// dids receives the display id when the callback owner is moved
    66  	// to a different screen.
    67  	dids chan uint64
    68  	// running tracks the desired state of the link. running is accessed
    69  	// with atomic.
    70  	running uint32
    71  }
    72  
    73  // displayLinks maps CFTypeRefs to *displayLinks.
    74  var displayLinks sync.Map
    75  
    76  var mainFuncs = make(chan func(), 1)
    77  
    78  // runOnMain runs the function on the main thread.
    79  func runOnMain(f func()) {
    80  	if C.isMainThread() {
    81  		f()
    82  		return
    83  	}
    84  	go func() {
    85  		mainFuncs <- f
    86  		C.gio_wakeupMainThread()
    87  	}()
    88  }
    89  
    90  //export gio_dispatchMainFuncs
    91  func gio_dispatchMainFuncs() {
    92  	for {
    93  		select {
    94  		case f := <-mainFuncs:
    95  			f()
    96  		default:
    97  			return
    98  		}
    99  	}
   100  }
   101  
   102  // nsstringToString converts a NSString to a Go string.
   103  func nsstringToString(str C.CFTypeRef) string {
   104  	if str == 0 {
   105  		return ""
   106  	}
   107  	n := C.nsstringLength(str)
   108  	if n == 0 {
   109  		return ""
   110  	}
   111  	chars := make([]uint16, n)
   112  	C.nsstringGetCharacters(str, (*C.unichar)(unsafe.Pointer(&chars[0])), 0, n)
   113  	utf8 := utf16.Decode(chars)
   114  	return string(utf8)
   115  }
   116  
   117  // stringToNSString converts a Go string to a retained NSString.
   118  func stringToNSString(str string) C.CFTypeRef {
   119  	u16 := utf16.Encode([]rune(str))
   120  	var chars *C.unichar
   121  	if len(u16) > 0 {
   122  		chars = (*C.unichar)(unsafe.Pointer(&u16[0]))
   123  	}
   124  	return C.newNSString(chars, C.NSUInteger(len(u16)))
   125  }
   126  
   127  func newDisplayLink(callback func()) (*displayLink, error) {
   128  	d := &displayLink{
   129  		callback: callback,
   130  		done:     make(chan struct{}),
   131  		states:   make(chan bool),
   132  		dids:     make(chan uint64),
   133  	}
   134  	dl := C.gio_createDisplayLink()
   135  	if dl == 0 {
   136  		return nil, errors.New("app: failed to create display link")
   137  	}
   138  	go d.run(dl)
   139  	return d, nil
   140  }
   141  
   142  func (d *displayLink) run(dl C.CFTypeRef) {
   143  	defer C.gio_releaseDisplayLink(dl)
   144  	displayLinks.Store(dl, d)
   145  	defer displayLinks.Delete(dl)
   146  	var stopTimer *time.Timer
   147  	var tchan <-chan time.Time
   148  	started := false
   149  	for {
   150  		select {
   151  		case <-tchan:
   152  			tchan = nil
   153  			started = false
   154  			C.gio_stopDisplayLink(dl)
   155  		case start := <-d.states:
   156  			switch {
   157  			case !start && tchan == nil:
   158  				// stopTimeout is the delay before stopping the display link to
   159  				// avoid the overhead of frequently starting and stopping the
   160  				// link thread.
   161  				const stopTimeout = 500 * time.Millisecond
   162  				if stopTimer == nil {
   163  					stopTimer = time.NewTimer(stopTimeout)
   164  				} else {
   165  					// stopTimer is always drained when tchan == nil.
   166  					stopTimer.Reset(stopTimeout)
   167  				}
   168  				tchan = stopTimer.C
   169  				atomic.StoreUint32(&d.running, 0)
   170  			case start:
   171  				if tchan != nil && !stopTimer.Stop() {
   172  					<-tchan
   173  				}
   174  				tchan = nil
   175  				atomic.StoreUint32(&d.running, 1)
   176  				if !started {
   177  					started = true
   178  					C.gio_startDisplayLink(dl)
   179  				}
   180  			}
   181  		case did := <-d.dids:
   182  			C.gio_setDisplayLinkDisplay(dl, C.uint64_t(did))
   183  		case <-d.done:
   184  			return
   185  		}
   186  	}
   187  }
   188  
   189  func (d *displayLink) Start() {
   190  	d.states <- true
   191  }
   192  
   193  func (d *displayLink) Stop() {
   194  	d.states <- false
   195  }
   196  
   197  func (d *displayLink) Close() {
   198  	close(d.done)
   199  }
   200  
   201  func (d *displayLink) SetDisplayID(did uint64) {
   202  	d.dids <- did
   203  }
   204  
   205  //export gio_onFrameCallback
   206  func gio_onFrameCallback(ref C.CFTypeRef) {
   207  	d, exists := displayLinks.Load(ref)
   208  	if !exists {
   209  		return
   210  	}
   211  	dl := d.(*displayLink)
   212  	if atomic.LoadUint32(&dl.running) != 0 {
   213  		dl.callback()
   214  	}
   215  }
   216  
   217  var macosCursorID = [...]byte{
   218  	pointer.CursorDefault:                  0,
   219  	pointer.CursorNone:                     1,
   220  	pointer.CursorText:                     2,
   221  	pointer.CursorVerticalText:             3,
   222  	pointer.CursorPointer:                  4,
   223  	pointer.CursorCrosshair:                5,
   224  	pointer.CursorAllScroll:                6,
   225  	pointer.CursorColResize:                7,
   226  	pointer.CursorRowResize:                8,
   227  	pointer.CursorGrab:                     9,
   228  	pointer.CursorGrabbing:                 10,
   229  	pointer.CursorNotAllowed:               11,
   230  	pointer.CursorWait:                     12,
   231  	pointer.CursorProgress:                 13,
   232  	pointer.CursorNorthWestResize:          14,
   233  	pointer.CursorNorthEastResize:          15,
   234  	pointer.CursorSouthWestResize:          16,
   235  	pointer.CursorSouthEastResize:          17,
   236  	pointer.CursorNorthSouthResize:         18,
   237  	pointer.CursorEastWestResize:           19,
   238  	pointer.CursorWestResize:               20,
   239  	pointer.CursorEastResize:               21,
   240  	pointer.CursorNorthResize:              22,
   241  	pointer.CursorSouthResize:              23,
   242  	pointer.CursorNorthEastSouthWestResize: 24,
   243  	pointer.CursorNorthWestSouthEastResize: 25,
   244  }
   245  
   246  // windowSetCursor updates the cursor from the current one to a new one
   247  // and returns the new one.
   248  func windowSetCursor(from, to pointer.Cursor) pointer.Cursor {
   249  	if from == to {
   250  		return to
   251  	}
   252  	if to == pointer.CursorNone {
   253  		C.gio_hideCursor()
   254  		return to
   255  	}
   256  	if from == pointer.CursorNone {
   257  		C.gio_showCursor()
   258  	}
   259  	C.gio_setCursor(C.NSUInteger(macosCursorID[to]))
   260  	return to
   261  }
   262  
   263  func (w *window) wakeup() {
   264  	runOnMain(func() {
   265  		w.loop.Wakeup()
   266  		w.loop.FlushEvents()
   267  	})
   268  }