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  }