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 }