github.com/goki/mobile@v0.0.0-20230707090321-193544ec5700/app/darwin_ios.go (about) 1 // Copyright 2015 The Go Authors. All rights reserved. 2 // Use of this source code is governed by a BSD-style 3 // license that can be found in the LICENSE file. 4 5 //go:build darwin && ios 6 // +build darwin,ios 7 8 package app 9 10 /* 11 #cgo CFLAGS: -x objective-c -DGL_SILENCE_DEPRECATION 12 #cgo LDFLAGS: -framework Foundation -framework UIKit -framework MobileCoreServices -framework GLKit -framework OpenGLES -framework QuartzCore -framework UserNotifications 13 #include <sys/utsname.h> 14 #include <stdint.h> 15 #include <stdbool.h> 16 #include <pthread.h> 17 #import <UIKit/UIKit.h> 18 #import <MobileCoreServices/MobileCoreServices.h> 19 #include <UIKit/UIDevice.h> 20 #import <GLKit/GLKit.h> 21 22 extern struct utsname sysInfo; 23 24 void runApp(void); 25 void makeCurrentContext(GLintptr ctx); 26 void swapBuffers(GLintptr ctx); 27 uint64_t threadID(); 28 29 UIEdgeInsets getDevicePadding(); 30 bool isDark(); 31 void showKeyboard(int keyboardType); 32 void hideKeyboard(); 33 34 void showFileOpenPicker(char* mimes, char *exts); 35 void showFileSavePicker(char* mimes, char *exts); 36 void closeFileResource(void* urlPtr); 37 */ 38 import "C" 39 import ( 40 "log" 41 "runtime" 42 "strings" 43 "time" 44 "unsafe" 45 46 "github.com/goki/mobile/event/lifecycle" 47 "github.com/goki/mobile/event/paint" 48 "github.com/goki/mobile/event/size" 49 "github.com/goki/mobile/event/touch" 50 "github.com/goki/mobile/geom" 51 ) 52 53 var initThreadID uint64 54 55 func init() { 56 // Lock the goroutine responsible for initialization to an OS thread. 57 // This means the goroutine running main (and calling the run function 58 // below) is locked to the OS thread that started the program. This is 59 // necessary for the correct delivery of UIKit events to the process. 60 // 61 // A discussion on this topic: 62 // https://groups.google.com/forum/#!msg/golang-nuts/IiWZ2hUuLDA/SNKYYZBelsYJ 63 runtime.LockOSThread() 64 initThreadID = uint64(C.threadID()) 65 } 66 67 func main(f func(App)) { 68 //if tid := uint64(C.threadID()); tid != initThreadID { 69 // log.Fatalf("app.Run called on thread %d, but app.init ran on %d", tid, initThreadID) 70 //} 71 72 log.Println("in mobile main") 73 go func() { 74 f(theApp) 75 // TODO(crawshaw): trigger runApp to return 76 }() 77 C.runApp() 78 panic("unexpected return from app.runApp") 79 } 80 81 var pixelsPerPt float32 82 var screenScale int // [UIScreen mainScreen].scale, either 1, 2, or 3. 83 84 var DisplayMetrics struct { 85 WidthPx int 86 HeightPx int 87 } 88 89 //export setWindowPtr 90 func setWindowPtr(window *C.void) { 91 theApp.window = uintptr(unsafe.Pointer(window)) 92 log.Println("set window pointer to:", theApp.window) 93 } 94 95 //export setDisplayMetrics 96 func setDisplayMetrics(width, height int, scale int) { 97 DisplayMetrics.WidthPx = width 98 DisplayMetrics.HeightPx = height 99 } 100 101 //export setScreen 102 func setScreen(scale int) { 103 C.uname(&C.sysInfo) 104 name := C.GoString(&C.sysInfo.machine[0]) 105 106 var v float32 107 108 switch { 109 case strings.HasPrefix(name, "iPhone"): 110 v = 163 111 case strings.HasPrefix(name, "iPad"): 112 // TODO: is there a better way to distinguish the iPad Mini? 113 switch name { 114 case "iPad2,5", "iPad2,6", "iPad2,7", "iPad4,4", "iPad4,5", "iPad4,6", "iPad4,7": 115 v = 163 // iPad Mini 116 default: 117 v = 132 118 } 119 default: 120 v = 163 // names like i386 and x86_64 are the simulator 121 } 122 123 if v == 0 { 124 log.Printf("unknown machine: %s", name) 125 v = 163 // emergency fallback 126 } 127 128 pixelsPerPt = v * float32(scale) / 72 129 screenScale = scale 130 } 131 132 //export updateConfig 133 func updateConfig(width, height, orientation int32) { 134 o := size.OrientationUnknown 135 switch orientation { 136 case C.UIDeviceOrientationPortrait, C.UIDeviceOrientationPortraitUpsideDown: 137 o = size.OrientationPortrait 138 case C.UIDeviceOrientationLandscapeLeft, C.UIDeviceOrientationLandscapeRight: 139 o = size.OrientationLandscape 140 width, height = height, width 141 } 142 insets := C.getDevicePadding() 143 144 theApp.eventsIn <- size.Event{ 145 WidthPx: int(width), 146 HeightPx: int(height), 147 WidthPt: geom.Pt(float32(width) / pixelsPerPt), 148 HeightPt: geom.Pt(float32(height) / pixelsPerPt), 149 InsetTopPx: int(float32(insets.top) * float32(screenScale)), 150 InsetBottomPx: int(float32(insets.bottom) * float32(screenScale)), 151 InsetLeftPx: int(float32(insets.left) * float32(screenScale)), 152 InsetRightPx: int(float32(insets.right) * float32(screenScale)), 153 PixelsPerPt: pixelsPerPt, 154 Orientation: o, 155 DarkMode: bool(C.isDark()), 156 } 157 theApp.eventsIn <- paint.Event{External: true} 158 } 159 160 // touchIDs is the current active touches. The position in the array 161 // is the ID, the value is the UITouch* pointer value. 162 // 163 // It is widely reported that the iPhone can handle up to 5 simultaneous 164 // touch events, while the iPad can handle 11. 165 var touchIDs [11]uintptr 166 167 //export sendTouch 168 func sendTouch(cTouch, cTouchType uintptr, x, y float32) { 169 id := -1 170 for i, val := range touchIDs { 171 if val == cTouch { 172 id = i 173 break 174 } 175 } 176 if id == -1 { 177 for i, val := range touchIDs { 178 if val == 0 { 179 touchIDs[i] = cTouch 180 id = i 181 break 182 } 183 } 184 if id == -1 { 185 panic("out of touchIDs") 186 } 187 } 188 189 t := touch.Type(cTouchType) 190 if t == touch.TypeEnd { 191 // Clear all touchIDs when touch ends. The UITouch pointers are unique 192 // at every multi-touch event. See: 193 // https://github.com/fyne-io/fyne/issues/2407 194 // https://developer.apple.com/documentation/uikit/touches_presses_and_gestures?language=objc 195 for idx := range touchIDs { 196 touchIDs[idx] = 0 197 } 198 } 199 200 theApp.eventsIn <- touch.Event{ 201 X: x, 202 Y: y, 203 Sequence: touch.Sequence(id), 204 Type: t, 205 } 206 } 207 208 //export lifecycleDead 209 func lifecycleDead() { theApp.sendLifecycle(lifecycle.StageDead) } 210 211 //export lifecycleAlive 212 func lifecycleAlive() { theApp.sendLifecycle(lifecycle.StageAlive) } 213 214 //export lifecycleVisible 215 func lifecycleVisible() { theApp.sendLifecycle(lifecycle.StageVisible) } 216 217 //export lifecycleFocused 218 func lifecycleFocused() { theApp.sendLifecycle(lifecycle.StageFocused) } 219 220 //export drawloop 221 func drawloop() { 222 runtime.LockOSThread() 223 defer runtime.UnlockOSThread() 224 225 // for workAvailable := theApp.worker.WorkAvailable(); ; { 226 for { 227 select { 228 // case <-workAvailable: 229 // theApp.worker.DoWork() 230 case <-theApp.publish: 231 theApp.publishResult <- PublishResult{} 232 return 233 case <-time.After(100 * time.Millisecond): // incase the method blocked!! 234 return 235 } 236 } 237 } 238 239 //export startloop 240 func startloop(ctx C.GLintptr) { 241 go theApp.loop(ctx) 242 } 243 244 // loop is the primary drawing loop. 245 // 246 // After UIKit has captured the initial OS thread for processing UIKit 247 // events in runApp, it starts loop on another goroutine. It is locked 248 // to an OS thread for its OpenGL context. 249 func (a *app) loop(ctx C.GLintptr) { 250 runtime.LockOSThread() 251 // C.makeCurrentContext(ctx) 252 253 // workAvailable := a.worker.WorkAvailable() 254 255 for { 256 select { 257 // case <-workAvailable: 258 // a.worker.DoWork() 259 case <-theApp.publish: 260 // loop1: 261 // for { 262 // select { 263 // case <-workAvailable: 264 // a.worker.DoWork() 265 // default: 266 // break loop1 267 // } 268 // } 269 // C.swapBuffers(ctx) 270 theApp.publishResult <- PublishResult{} 271 } 272 } 273 } 274 275 func cStringsForFilter(filter *FileFilter) (*C.char, *C.char) { 276 mimes := strings.Join(filter.MimeTypes, "|") 277 278 // extensions must have the '.' removed for UTI lookups on iOS 279 extList := []string{} 280 for _, ext := range filter.Extensions { 281 extList = append(extList, ext[1:]) 282 } 283 exts := strings.Join(extList, "|") 284 285 return C.CString(mimes), C.CString(exts) 286 } 287 288 // driverShowVirtualKeyboard requests the driver to show a virtual keyboard for text input 289 func driverShowVirtualKeyboard(keyboard KeyboardType) { 290 C.showKeyboard(C.int(int32(keyboard))) 291 } 292 293 // driverHideVirtualKeyboard requests the driver to hide any visible virtual keyboard 294 func driverHideVirtualKeyboard() { 295 C.hideKeyboard() 296 } 297 298 var fileCallback func(string, func()) 299 300 //export filePickerReturned 301 func filePickerReturned(str *C.char, urlPtr unsafe.Pointer) { 302 if fileCallback == nil { 303 return 304 } 305 306 fileCallback(C.GoString(str), func() { 307 C.closeFileResource(urlPtr) 308 }) 309 fileCallback = nil 310 } 311 312 func driverShowFileOpenPicker(callback func(string, func()), filter *FileFilter) { 313 fileCallback = callback 314 315 mimeStr, extStr := cStringsForFilter(filter) 316 defer C.free(unsafe.Pointer(mimeStr)) 317 defer C.free(unsafe.Pointer(extStr)) 318 319 C.showFileOpenPicker(mimeStr, extStr) 320 } 321 322 func driverShowFileSavePicker(callback func(string, func()), filter *FileFilter, filename string) { 323 fileCallback = callback 324 325 mimeStr, extStr := cStringsForFilter(filter) 326 defer C.free(unsafe.Pointer(mimeStr)) 327 defer C.free(unsafe.Pointer(extStr)) 328 329 C.showFileSavePicker(mimeStr, extStr) 330 }