Adding upstream version 0.0~git20250520.a1d9079+dfsg.
Signed-off-by: Daniel Baumann <daniel@debian.org>
This commit is contained in:
parent
590ac7ff5f
commit
20149b7f3a
456 changed files with 70406 additions and 0 deletions
67
app/GoNativeActivity.java
Normal file
67
app/GoNativeActivity.java
Normal file
|
@ -0,0 +1,67 @@
|
|||
package org.golang.app;
|
||||
|
||||
import android.app.Activity;
|
||||
import android.app.NativeActivity;
|
||||
import android.content.Context;
|
||||
import android.content.pm.ActivityInfo;
|
||||
import android.content.pm.PackageManager;
|
||||
import android.os.Bundle;
|
||||
import android.util.Log;
|
||||
import android.view.KeyCharacterMap;
|
||||
|
||||
public class GoNativeActivity extends NativeActivity {
|
||||
private static GoNativeActivity goNativeActivity;
|
||||
|
||||
public GoNativeActivity() {
|
||||
super();
|
||||
goNativeActivity = this;
|
||||
}
|
||||
|
||||
String getTmpdir() {
|
||||
return getCacheDir().getAbsolutePath();
|
||||
}
|
||||
|
||||
static int getRune(int deviceId, int keyCode, int metaState) {
|
||||
try {
|
||||
int rune = KeyCharacterMap.load(deviceId).get(keyCode, metaState);
|
||||
if (rune == 0) {
|
||||
return -1;
|
||||
}
|
||||
return rune;
|
||||
} catch (KeyCharacterMap.UnavailableException e) {
|
||||
return -1;
|
||||
} catch (Exception e) {
|
||||
Log.e("Go", "exception reading KeyCharacterMap", e);
|
||||
return -1;
|
||||
}
|
||||
}
|
||||
|
||||
private void load() {
|
||||
// Interestingly, NativeActivity uses a different method
|
||||
// to find native code to execute, avoiding
|
||||
// System.loadLibrary. The result is Java methods
|
||||
// implemented in C with JNIEXPORT (and JNI_OnLoad) are not
|
||||
// available unless an explicit call to System.loadLibrary
|
||||
// is done. So we do it here, borrowing the name of the
|
||||
// library from the same AndroidManifest.xml metadata used
|
||||
// by NativeActivity.
|
||||
try {
|
||||
ActivityInfo ai = getPackageManager().getActivityInfo(
|
||||
getIntent().getComponent(), PackageManager.GET_META_DATA);
|
||||
if (ai.metaData == null) {
|
||||
Log.e("Go", "loadLibrary: no manifest metadata found");
|
||||
return;
|
||||
}
|
||||
String libName = ai.metaData.getString("android.app.lib_name");
|
||||
System.loadLibrary(libName);
|
||||
} catch (Exception e) {
|
||||
Log.e("Go", "loadLibrary failed", e);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onCreate(Bundle savedInstanceState) {
|
||||
load();
|
||||
super.onCreate(savedInstanceState);
|
||||
}
|
||||
}
|
201
app/android.c
Normal file
201
app/android.c
Normal file
|
@ -0,0 +1,201 @@
|
|||
// Copyright 2014 The Go Authors. All rights reserved.
|
||||
// Use of this source code is governed by a BSD-style
|
||||
// license that can be found in the LICENSE file.
|
||||
|
||||
//go:build android
|
||||
// +build android
|
||||
|
||||
#include <android/log.h>
|
||||
#include <dlfcn.h>
|
||||
#include <errno.h>
|
||||
#include <fcntl.h>
|
||||
#include <stdint.h>
|
||||
#include <string.h>
|
||||
#include "_cgo_export.h"
|
||||
|
||||
#define LOG_INFO(...) __android_log_print(ANDROID_LOG_INFO, "Go", __VA_ARGS__)
|
||||
#define LOG_FATAL(...) __android_log_print(ANDROID_LOG_FATAL, "Go", __VA_ARGS__)
|
||||
|
||||
static jclass current_class;
|
||||
|
||||
static jclass find_class(JNIEnv *env, const char *class_name) {
|
||||
jclass clazz = (*env)->FindClass(env, class_name);
|
||||
if (clazz == NULL) {
|
||||
(*env)->ExceptionClear(env);
|
||||
LOG_FATAL("cannot find %s", class_name);
|
||||
return NULL;
|
||||
}
|
||||
return clazz;
|
||||
}
|
||||
|
||||
static jmethodID find_method(JNIEnv *env, jclass clazz, const char *name, const char *sig) {
|
||||
jmethodID m = (*env)->GetMethodID(env, clazz, name, sig);
|
||||
if (m == 0) {
|
||||
(*env)->ExceptionClear(env);
|
||||
LOG_FATAL("cannot find method %s %s", name, sig);
|
||||
return 0;
|
||||
}
|
||||
return m;
|
||||
}
|
||||
|
||||
static jmethodID find_static_method(JNIEnv *env, jclass clazz, const char *name, const char *sig) {
|
||||
jmethodID m = (*env)->GetStaticMethodID(env, clazz, name, sig);
|
||||
if (m == 0) {
|
||||
(*env)->ExceptionClear(env);
|
||||
LOG_FATAL("cannot find method %s %s", name, sig);
|
||||
return 0;
|
||||
}
|
||||
return m;
|
||||
}
|
||||
|
||||
static jmethodID key_rune_method;
|
||||
|
||||
jint JNI_OnLoad(JavaVM* vm, void* reserved) {
|
||||
JNIEnv* env;
|
||||
if ((*vm)->GetEnv(vm, (void**)&env, JNI_VERSION_1_6) != JNI_OK) {
|
||||
return -1;
|
||||
}
|
||||
|
||||
return JNI_VERSION_1_6;
|
||||
}
|
||||
|
||||
static int main_running = 0;
|
||||
|
||||
// Entry point from our subclassed NativeActivity.
|
||||
//
|
||||
// By here, the Go runtime has been initialized (as we are running in
|
||||
// -buildmode=c-shared) but the first time it is called, Go's main.main
|
||||
// hasn't been called yet.
|
||||
//
|
||||
// The Activity may be created and destroyed multiple times throughout
|
||||
// the life of a single process. Each time, onCreate is called.
|
||||
void ANativeActivity_onCreate(ANativeActivity *activity, void* savedState, size_t savedStateSize) {
|
||||
if (!main_running) {
|
||||
JNIEnv* env = activity->env;
|
||||
|
||||
// Note that activity->clazz is mis-named.
|
||||
current_class = (*env)->GetObjectClass(env, activity->clazz);
|
||||
current_class = (*env)->NewGlobalRef(env, current_class);
|
||||
key_rune_method = find_static_method(env, current_class, "getRune", "(III)I");
|
||||
|
||||
setCurrentContext(activity->vm, (*env)->NewGlobalRef(env, activity->clazz));
|
||||
|
||||
// Set TMPDIR.
|
||||
jmethodID gettmpdir = find_method(env, current_class, "getTmpdir", "()Ljava/lang/String;");
|
||||
jstring jpath = (jstring)(*env)->CallObjectMethod(env, activity->clazz, gettmpdir, NULL);
|
||||
const char* tmpdir = (*env)->GetStringUTFChars(env, jpath, NULL);
|
||||
if (setenv("TMPDIR", tmpdir, 1) != 0) {
|
||||
LOG_INFO("setenv(\"TMPDIR\", \"%s\", 1) failed: %d", tmpdir, errno);
|
||||
}
|
||||
(*env)->ReleaseStringUTFChars(env, jpath, tmpdir);
|
||||
|
||||
// Call the Go main.main.
|
||||
uintptr_t mainPC = (uintptr_t)dlsym(RTLD_DEFAULT, "main.main");
|
||||
if (!mainPC) {
|
||||
LOG_FATAL("missing main.main");
|
||||
}
|
||||
callMain(mainPC);
|
||||
main_running = 1;
|
||||
}
|
||||
|
||||
// These functions match the methods on Activity, described at
|
||||
// http://developer.android.com/reference/android/app/Activity.html
|
||||
//
|
||||
// Note that onNativeWindowResized is not called on resize. Avoid it.
|
||||
// https://code.google.com/p/android/issues/detail?id=180645
|
||||
activity->callbacks->onStart = onStart;
|
||||
activity->callbacks->onResume = onResume;
|
||||
activity->callbacks->onSaveInstanceState = onSaveInstanceState;
|
||||
activity->callbacks->onPause = onPause;
|
||||
activity->callbacks->onStop = onStop;
|
||||
activity->callbacks->onDestroy = onDestroy;
|
||||
activity->callbacks->onWindowFocusChanged = onWindowFocusChanged;
|
||||
activity->callbacks->onNativeWindowCreated = onNativeWindowCreated;
|
||||
activity->callbacks->onNativeWindowRedrawNeeded = onNativeWindowRedrawNeeded;
|
||||
activity->callbacks->onNativeWindowDestroyed = onNativeWindowDestroyed;
|
||||
activity->callbacks->onInputQueueCreated = onInputQueueCreated;
|
||||
activity->callbacks->onInputQueueDestroyed = onInputQueueDestroyed;
|
||||
activity->callbacks->onConfigurationChanged = onConfigurationChanged;
|
||||
activity->callbacks->onLowMemory = onLowMemory;
|
||||
|
||||
onCreate(activity);
|
||||
}
|
||||
|
||||
// TODO(crawshaw): Test configuration on more devices.
|
||||
static const EGLint RGB_888[] = {
|
||||
EGL_RENDERABLE_TYPE, EGL_OPENGL_ES2_BIT,
|
||||
EGL_SURFACE_TYPE, EGL_WINDOW_BIT,
|
||||
EGL_BLUE_SIZE, 8,
|
||||
EGL_GREEN_SIZE, 8,
|
||||
EGL_RED_SIZE, 8,
|
||||
EGL_DEPTH_SIZE, 16,
|
||||
EGL_CONFIG_CAVEAT, EGL_NONE,
|
||||
EGL_NONE
|
||||
};
|
||||
|
||||
EGLDisplay display = NULL;
|
||||
EGLSurface surface = NULL;
|
||||
|
||||
static char* initEGLDisplay() {
|
||||
display = eglGetDisplay(EGL_DEFAULT_DISPLAY);
|
||||
if (!eglInitialize(display, 0, 0)) {
|
||||
return "EGL initialize failed";
|
||||
}
|
||||
return NULL;
|
||||
}
|
||||
|
||||
char* createEGLSurface(ANativeWindow* window) {
|
||||
char* err;
|
||||
EGLint numConfigs, format;
|
||||
EGLConfig config;
|
||||
EGLContext context;
|
||||
|
||||
if (display == 0) {
|
||||
if ((err = initEGLDisplay()) != NULL) {
|
||||
return err;
|
||||
}
|
||||
}
|
||||
|
||||
if (!eglChooseConfig(display, RGB_888, &config, 1, &numConfigs)) {
|
||||
return "EGL choose RGB_888 config failed";
|
||||
}
|
||||
if (numConfigs <= 0) {
|
||||
return "EGL no config found";
|
||||
}
|
||||
|
||||
eglGetConfigAttrib(display, config, EGL_NATIVE_VISUAL_ID, &format);
|
||||
if (ANativeWindow_setBuffersGeometry(window, 0, 0, format) != 0) {
|
||||
return "EGL set buffers geometry failed";
|
||||
}
|
||||
|
||||
surface = eglCreateWindowSurface(display, config, window, NULL);
|
||||
if (surface == EGL_NO_SURFACE) {
|
||||
return "EGL create surface failed";
|
||||
}
|
||||
|
||||
const EGLint contextAttribs[] = { EGL_CONTEXT_CLIENT_VERSION, 2, EGL_NONE };
|
||||
context = eglCreateContext(display, config, EGL_NO_CONTEXT, contextAttribs);
|
||||
|
||||
if (eglMakeCurrent(display, surface, surface, context) == EGL_FALSE) {
|
||||
return "eglMakeCurrent failed";
|
||||
}
|
||||
return NULL;
|
||||
}
|
||||
|
||||
char* destroyEGLSurface() {
|
||||
if (!eglDestroySurface(display, surface)) {
|
||||
return "EGL destroy surface failed";
|
||||
}
|
||||
return NULL;
|
||||
}
|
||||
|
||||
int32_t getKeyRune(JNIEnv* env, AInputEvent* e) {
|
||||
return (int32_t)(*env)->CallStaticIntMethod(
|
||||
env,
|
||||
current_class,
|
||||
key_rune_method,
|
||||
AInputEvent_getDeviceId(e),
|
||||
AKeyEvent_getKeyCode(e),
|
||||
AKeyEvent_getMetaState(e)
|
||||
);
|
||||
}
|
824
app/android.go
Normal file
824
app/android.go
Normal file
|
@ -0,0 +1,824 @@
|
|||
// Copyright 2014 The Go Authors. All rights reserved.
|
||||
// Use of this source code is governed by a BSD-style
|
||||
// license that can be found in the LICENSE file.
|
||||
|
||||
//go:build android
|
||||
|
||||
/*
|
||||
Android Apps are built with -buildmode=c-shared. They are loaded by a
|
||||
running Java process.
|
||||
|
||||
Before any entry point is reached, a global constructor initializes the
|
||||
Go runtime, calling all Go init functions. All cgo calls will block
|
||||
until this is complete. Next JNI_OnLoad is called. When that is
|
||||
complete, one of two entry points is called.
|
||||
|
||||
All-Go apps built using NativeActivity enter at ANativeActivity_onCreate.
|
||||
|
||||
Go libraries (for example, those built with gomobile bind) do not use
|
||||
the app package initialization.
|
||||
*/
|
||||
|
||||
package app
|
||||
|
||||
/*
|
||||
#cgo LDFLAGS: -landroid -llog -lEGL -lGLESv2
|
||||
|
||||
#include <android/configuration.h>
|
||||
#include <android/input.h>
|
||||
#include <android/keycodes.h>
|
||||
#include <android/looper.h>
|
||||
#include <android/native_activity.h>
|
||||
#include <android/native_window.h>
|
||||
#include <EGL/egl.h>
|
||||
#include <jni.h>
|
||||
#include <pthread.h>
|
||||
#include <stdlib.h>
|
||||
|
||||
extern EGLDisplay display;
|
||||
extern EGLSurface surface;
|
||||
|
||||
|
||||
char* createEGLSurface(ANativeWindow* window);
|
||||
char* destroyEGLSurface();
|
||||
int32_t getKeyRune(JNIEnv* env, AInputEvent* e);
|
||||
*/
|
||||
import "C"
|
||||
import (
|
||||
"fmt"
|
||||
"log"
|
||||
"os"
|
||||
"time"
|
||||
"unsafe"
|
||||
|
||||
"golang.org/x/mobile/app/internal/callfn"
|
||||
"golang.org/x/mobile/event/key"
|
||||
"golang.org/x/mobile/event/lifecycle"
|
||||
"golang.org/x/mobile/event/paint"
|
||||
"golang.org/x/mobile/event/size"
|
||||
"golang.org/x/mobile/event/touch"
|
||||
"golang.org/x/mobile/geom"
|
||||
"golang.org/x/mobile/internal/mobileinit"
|
||||
)
|
||||
|
||||
// RunOnJVM runs fn on a new goroutine locked to an OS thread with a JNIEnv.
|
||||
//
|
||||
// RunOnJVM blocks until the call to fn is complete. Any Java
|
||||
// exception or failure to attach to the JVM is returned as an error.
|
||||
//
|
||||
// The function fn takes vm, the current JavaVM*,
|
||||
// env, the current JNIEnv*, and
|
||||
// ctx, a jobject representing the global android.context.Context.
|
||||
func RunOnJVM(fn func(vm, jniEnv, ctx uintptr) error) error {
|
||||
return mobileinit.RunOnJVM(fn)
|
||||
}
|
||||
|
||||
//export setCurrentContext
|
||||
func setCurrentContext(vm *C.JavaVM, ctx C.jobject) {
|
||||
mobileinit.SetCurrentContext(unsafe.Pointer(vm), uintptr(ctx))
|
||||
}
|
||||
|
||||
//export callMain
|
||||
func callMain(mainPC uintptr) {
|
||||
for _, name := range []string{"TMPDIR", "PATH", "LD_LIBRARY_PATH"} {
|
||||
n := C.CString(name)
|
||||
os.Setenv(name, C.GoString(C.getenv(n)))
|
||||
C.free(unsafe.Pointer(n))
|
||||
}
|
||||
|
||||
// Set timezone.
|
||||
//
|
||||
// Note that Android zoneinfo is stored in /system/usr/share/zoneinfo,
|
||||
// but it is in some kind of packed TZiff file that we do not support
|
||||
// yet. As a stopgap, we build a fixed zone using the tm_zone name.
|
||||
var curtime C.time_t
|
||||
var curtm C.struct_tm
|
||||
C.time(&curtime)
|
||||
C.localtime_r(&curtime, &curtm)
|
||||
tzOffset := int(curtm.tm_gmtoff)
|
||||
tz := C.GoString(curtm.tm_zone)
|
||||
time.Local = time.FixedZone(tz, tzOffset)
|
||||
|
||||
go callfn.CallFn(mainPC)
|
||||
}
|
||||
|
||||
//export onStart
|
||||
func onStart(activity *C.ANativeActivity) {
|
||||
}
|
||||
|
||||
//export onResume
|
||||
func onResume(activity *C.ANativeActivity) {
|
||||
}
|
||||
|
||||
//export onSaveInstanceState
|
||||
func onSaveInstanceState(activity *C.ANativeActivity, outSize *C.size_t) unsafe.Pointer {
|
||||
return nil
|
||||
}
|
||||
|
||||
//export onPause
|
||||
func onPause(activity *C.ANativeActivity) {
|
||||
}
|
||||
|
||||
//export onStop
|
||||
func onStop(activity *C.ANativeActivity) {
|
||||
}
|
||||
|
||||
//export onCreate
|
||||
func onCreate(activity *C.ANativeActivity) {
|
||||
// Set the initial configuration.
|
||||
//
|
||||
// Note we use unbuffered channels to talk to the activity loop, and
|
||||
// NativeActivity calls these callbacks sequentially, so configuration
|
||||
// will be set before <-windowRedrawNeeded is processed.
|
||||
windowConfigChange <- windowConfigRead(activity)
|
||||
}
|
||||
|
||||
//export onDestroy
|
||||
func onDestroy(activity *C.ANativeActivity) {
|
||||
}
|
||||
|
||||
//export onWindowFocusChanged
|
||||
func onWindowFocusChanged(activity *C.ANativeActivity, hasFocus C.int) {
|
||||
}
|
||||
|
||||
//export onNativeWindowCreated
|
||||
func onNativeWindowCreated(activity *C.ANativeActivity, window *C.ANativeWindow) {
|
||||
}
|
||||
|
||||
//export onNativeWindowRedrawNeeded
|
||||
func onNativeWindowRedrawNeeded(activity *C.ANativeActivity, window *C.ANativeWindow) {
|
||||
// Called on orientation change and window resize.
|
||||
// Send a request for redraw, and block this function
|
||||
// until a complete draw and buffer swap is completed.
|
||||
// This is required by the redraw documentation to
|
||||
// avoid bad draws.
|
||||
windowRedrawNeeded <- window
|
||||
<-windowRedrawDone
|
||||
}
|
||||
|
||||
//export onNativeWindowDestroyed
|
||||
func onNativeWindowDestroyed(activity *C.ANativeActivity, window *C.ANativeWindow) {
|
||||
windowDestroyed <- window
|
||||
}
|
||||
|
||||
//export onInputQueueCreated
|
||||
func onInputQueueCreated(activity *C.ANativeActivity, q *C.AInputQueue) {
|
||||
inputQueue <- q
|
||||
<-inputQueueDone
|
||||
}
|
||||
|
||||
//export onInputQueueDestroyed
|
||||
func onInputQueueDestroyed(activity *C.ANativeActivity, q *C.AInputQueue) {
|
||||
inputQueue <- nil
|
||||
<-inputQueueDone
|
||||
}
|
||||
|
||||
//export onContentRectChanged
|
||||
func onContentRectChanged(activity *C.ANativeActivity, rect *C.ARect) {
|
||||
}
|
||||
|
||||
type windowConfig struct {
|
||||
orientation size.Orientation
|
||||
pixelsPerPt float32
|
||||
}
|
||||
|
||||
func windowConfigRead(activity *C.ANativeActivity) windowConfig {
|
||||
aconfig := C.AConfiguration_new()
|
||||
C.AConfiguration_fromAssetManager(aconfig, activity.assetManager)
|
||||
orient := C.AConfiguration_getOrientation(aconfig)
|
||||
density := C.AConfiguration_getDensity(aconfig)
|
||||
C.AConfiguration_delete(aconfig)
|
||||
|
||||
// Calculate the screen resolution. This value is approximate. For example,
|
||||
// a physical resolution of 200 DPI may be quantized to one of the
|
||||
// ACONFIGURATION_DENSITY_XXX values such as 160 or 240.
|
||||
//
|
||||
// A more accurate DPI could possibly be calculated from
|
||||
// https://developer.android.com/reference/android/util/DisplayMetrics.html#xdpi
|
||||
// but this does not appear to be accessible via the NDK. In any case, the
|
||||
// hardware might not even provide a more accurate number, as the system
|
||||
// does not apparently use the reported value. See golang.org/issue/13366
|
||||
// for a discussion.
|
||||
var dpi int
|
||||
switch density {
|
||||
case C.ACONFIGURATION_DENSITY_DEFAULT:
|
||||
dpi = 160
|
||||
case C.ACONFIGURATION_DENSITY_LOW,
|
||||
C.ACONFIGURATION_DENSITY_MEDIUM,
|
||||
213, // C.ACONFIGURATION_DENSITY_TV
|
||||
C.ACONFIGURATION_DENSITY_HIGH,
|
||||
320, // ACONFIGURATION_DENSITY_XHIGH
|
||||
480, // ACONFIGURATION_DENSITY_XXHIGH
|
||||
640: // ACONFIGURATION_DENSITY_XXXHIGH
|
||||
dpi = int(density)
|
||||
case C.ACONFIGURATION_DENSITY_NONE:
|
||||
log.Print("android device reports no screen density")
|
||||
dpi = 72
|
||||
default:
|
||||
log.Printf("android device reports unknown density: %d", density)
|
||||
// All we can do is guess.
|
||||
if density > 0 {
|
||||
dpi = int(density)
|
||||
} else {
|
||||
dpi = 72
|
||||
}
|
||||
}
|
||||
|
||||
o := size.OrientationUnknown
|
||||
switch orient {
|
||||
case C.ACONFIGURATION_ORIENTATION_PORT:
|
||||
o = size.OrientationPortrait
|
||||
case C.ACONFIGURATION_ORIENTATION_LAND:
|
||||
o = size.OrientationLandscape
|
||||
}
|
||||
|
||||
return windowConfig{
|
||||
orientation: o,
|
||||
pixelsPerPt: float32(dpi) / 72,
|
||||
}
|
||||
}
|
||||
|
||||
//export onConfigurationChanged
|
||||
func onConfigurationChanged(activity *C.ANativeActivity) {
|
||||
// A rotation event first triggers onConfigurationChanged, then
|
||||
// calls onNativeWindowRedrawNeeded. We extract the orientation
|
||||
// here and save it for the redraw event.
|
||||
windowConfigChange <- windowConfigRead(activity)
|
||||
}
|
||||
|
||||
//export onLowMemory
|
||||
func onLowMemory(activity *C.ANativeActivity) {
|
||||
}
|
||||
|
||||
var (
|
||||
inputQueue = make(chan *C.AInputQueue)
|
||||
inputQueueDone = make(chan struct{})
|
||||
windowDestroyed = make(chan *C.ANativeWindow)
|
||||
windowRedrawNeeded = make(chan *C.ANativeWindow)
|
||||
windowRedrawDone = make(chan struct{})
|
||||
windowConfigChange = make(chan windowConfig)
|
||||
)
|
||||
|
||||
func init() {
|
||||
theApp.registerGLViewportFilter()
|
||||
}
|
||||
|
||||
func main(f func(App)) {
|
||||
mainUserFn = f
|
||||
// TODO: merge the runInputQueue and mainUI functions?
|
||||
go func() {
|
||||
if err := mobileinit.RunOnJVM(runInputQueue); err != nil {
|
||||
log.Fatalf("app: %v", err)
|
||||
}
|
||||
}()
|
||||
// Preserve this OS thread for:
|
||||
// 1. the attached JNI thread
|
||||
// 2. the GL context
|
||||
if err := mobileinit.RunOnJVM(mainUI); err != nil {
|
||||
log.Fatalf("app: %v", err)
|
||||
}
|
||||
}
|
||||
|
||||
var mainUserFn func(App)
|
||||
|
||||
func mainUI(vm, jniEnv, ctx uintptr) error {
|
||||
workAvailable := theApp.worker.WorkAvailable()
|
||||
|
||||
donec := make(chan struct{})
|
||||
go func() {
|
||||
// close the donec channel in a defer statement
|
||||
// so that we could still be able to return even
|
||||
// if mainUserFn panics.
|
||||
defer close(donec)
|
||||
|
||||
mainUserFn(theApp)
|
||||
}()
|
||||
|
||||
var pixelsPerPt float32
|
||||
var orientation size.Orientation
|
||||
|
||||
for {
|
||||
select {
|
||||
case <-donec:
|
||||
return nil
|
||||
case cfg := <-windowConfigChange:
|
||||
pixelsPerPt = cfg.pixelsPerPt
|
||||
orientation = cfg.orientation
|
||||
case w := <-windowRedrawNeeded:
|
||||
if C.surface == nil {
|
||||
if errStr := C.createEGLSurface(w); errStr != nil {
|
||||
return fmt.Errorf("%s (%s)", C.GoString(errStr), eglGetError())
|
||||
}
|
||||
}
|
||||
theApp.sendLifecycle(lifecycle.StageFocused)
|
||||
widthPx := int(C.ANativeWindow_getWidth(w))
|
||||
heightPx := int(C.ANativeWindow_getHeight(w))
|
||||
theApp.eventsIn <- size.Event{
|
||||
WidthPx: widthPx,
|
||||
HeightPx: heightPx,
|
||||
WidthPt: geom.Pt(float32(widthPx) / pixelsPerPt),
|
||||
HeightPt: geom.Pt(float32(heightPx) / pixelsPerPt),
|
||||
PixelsPerPt: pixelsPerPt,
|
||||
Orientation: orientation,
|
||||
}
|
||||
theApp.eventsIn <- paint.Event{External: true}
|
||||
case <-windowDestroyed:
|
||||
if C.surface != nil {
|
||||
if errStr := C.destroyEGLSurface(); errStr != nil {
|
||||
return fmt.Errorf("%s (%s)", C.GoString(errStr), eglGetError())
|
||||
}
|
||||
}
|
||||
C.surface = nil
|
||||
theApp.sendLifecycle(lifecycle.StageAlive)
|
||||
case <-workAvailable:
|
||||
theApp.worker.DoWork()
|
||||
case <-theApp.publish:
|
||||
// TODO: compare a generation number to redrawGen for stale paints?
|
||||
if C.surface != nil {
|
||||
// eglSwapBuffers blocks until vsync.
|
||||
if C.eglSwapBuffers(C.display, C.surface) == C.EGL_FALSE {
|
||||
log.Printf("app: failed to swap buffers (%s)", eglGetError())
|
||||
}
|
||||
}
|
||||
select {
|
||||
case windowRedrawDone <- struct{}{}:
|
||||
default:
|
||||
}
|
||||
theApp.publishResult <- PublishResult{}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func runInputQueue(vm, jniEnv, ctx uintptr) error {
|
||||
env := (*C.JNIEnv)(unsafe.Pointer(jniEnv)) // not a Go heap pointer
|
||||
|
||||
// Android loopers select on OS file descriptors, not Go channels, so we
|
||||
// translate the inputQueue channel to an ALooper_wake call.
|
||||
l := C.ALooper_prepare(C.ALOOPER_PREPARE_ALLOW_NON_CALLBACKS)
|
||||
pending := make(chan *C.AInputQueue, 1)
|
||||
go func() {
|
||||
for q := range inputQueue {
|
||||
pending <- q
|
||||
C.ALooper_wake(l)
|
||||
}
|
||||
}()
|
||||
|
||||
var q *C.AInputQueue
|
||||
for {
|
||||
if C.ALooper_pollOnce(-1, nil, nil, nil) == C.ALOOPER_POLL_WAKE {
|
||||
select {
|
||||
default:
|
||||
case p := <-pending:
|
||||
if q != nil {
|
||||
processEvents(env, q)
|
||||
C.AInputQueue_detachLooper(q)
|
||||
}
|
||||
q = p
|
||||
if q != nil {
|
||||
C.AInputQueue_attachLooper(q, l, 0, nil, nil)
|
||||
}
|
||||
inputQueueDone <- struct{}{}
|
||||
}
|
||||
}
|
||||
if q != nil {
|
||||
processEvents(env, q)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func processEvents(env *C.JNIEnv, q *C.AInputQueue) {
|
||||
var e *C.AInputEvent
|
||||
for C.AInputQueue_getEvent(q, &e) >= 0 {
|
||||
if C.AInputQueue_preDispatchEvent(q, e) != 0 {
|
||||
continue
|
||||
}
|
||||
processEvent(env, e)
|
||||
C.AInputQueue_finishEvent(q, e, 0)
|
||||
}
|
||||
}
|
||||
|
||||
func processEvent(env *C.JNIEnv, e *C.AInputEvent) {
|
||||
switch C.AInputEvent_getType(e) {
|
||||
case C.AINPUT_EVENT_TYPE_KEY:
|
||||
processKey(env, e)
|
||||
case C.AINPUT_EVENT_TYPE_MOTION:
|
||||
// At most one of the events in this batch is an up or down event; get its index and change.
|
||||
upDownIndex := C.size_t(C.AMotionEvent_getAction(e)&C.AMOTION_EVENT_ACTION_POINTER_INDEX_MASK) >> C.AMOTION_EVENT_ACTION_POINTER_INDEX_SHIFT
|
||||
upDownType := touch.TypeMove
|
||||
switch C.AMotionEvent_getAction(e) & C.AMOTION_EVENT_ACTION_MASK {
|
||||
case C.AMOTION_EVENT_ACTION_DOWN, C.AMOTION_EVENT_ACTION_POINTER_DOWN:
|
||||
upDownType = touch.TypeBegin
|
||||
case C.AMOTION_EVENT_ACTION_UP, C.AMOTION_EVENT_ACTION_POINTER_UP:
|
||||
upDownType = touch.TypeEnd
|
||||
}
|
||||
|
||||
for i, n := C.size_t(0), C.AMotionEvent_getPointerCount(e); i < n; i++ {
|
||||
t := touch.TypeMove
|
||||
if i == upDownIndex {
|
||||
t = upDownType
|
||||
}
|
||||
theApp.eventsIn <- touch.Event{
|
||||
X: float32(C.AMotionEvent_getX(e, i)),
|
||||
Y: float32(C.AMotionEvent_getY(e, i)),
|
||||
Sequence: touch.Sequence(C.AMotionEvent_getPointerId(e, i)),
|
||||
Type: t,
|
||||
}
|
||||
}
|
||||
default:
|
||||
log.Printf("unknown input event, type=%d", C.AInputEvent_getType(e))
|
||||
}
|
||||
}
|
||||
|
||||
func processKey(env *C.JNIEnv, e *C.AInputEvent) {
|
||||
deviceID := C.AInputEvent_getDeviceId(e)
|
||||
if deviceID == 0 {
|
||||
// Software keyboard input, leaving for scribe/IME.
|
||||
return
|
||||
}
|
||||
|
||||
k := key.Event{
|
||||
Rune: rune(C.getKeyRune(env, e)),
|
||||
Code: convAndroidKeyCode(int32(C.AKeyEvent_getKeyCode(e))),
|
||||
}
|
||||
switch C.AKeyEvent_getAction(e) {
|
||||
case C.AKEY_EVENT_ACTION_DOWN:
|
||||
k.Direction = key.DirPress
|
||||
case C.AKEY_EVENT_ACTION_UP:
|
||||
k.Direction = key.DirRelease
|
||||
default:
|
||||
k.Direction = key.DirNone
|
||||
}
|
||||
// TODO(crawshaw): set Modifiers.
|
||||
theApp.eventsIn <- k
|
||||
}
|
||||
|
||||
func eglGetError() string {
|
||||
switch errNum := C.eglGetError(); errNum {
|
||||
case C.EGL_SUCCESS:
|
||||
return "EGL_SUCCESS"
|
||||
case C.EGL_NOT_INITIALIZED:
|
||||
return "EGL_NOT_INITIALIZED"
|
||||
case C.EGL_BAD_ACCESS:
|
||||
return "EGL_BAD_ACCESS"
|
||||
case C.EGL_BAD_ALLOC:
|
||||
return "EGL_BAD_ALLOC"
|
||||
case C.EGL_BAD_ATTRIBUTE:
|
||||
return "EGL_BAD_ATTRIBUTE"
|
||||
case C.EGL_BAD_CONTEXT:
|
||||
return "EGL_BAD_CONTEXT"
|
||||
case C.EGL_BAD_CONFIG:
|
||||
return "EGL_BAD_CONFIG"
|
||||
case C.EGL_BAD_CURRENT_SURFACE:
|
||||
return "EGL_BAD_CURRENT_SURFACE"
|
||||
case C.EGL_BAD_DISPLAY:
|
||||
return "EGL_BAD_DISPLAY"
|
||||
case C.EGL_BAD_SURFACE:
|
||||
return "EGL_BAD_SURFACE"
|
||||
case C.EGL_BAD_MATCH:
|
||||
return "EGL_BAD_MATCH"
|
||||
case C.EGL_BAD_PARAMETER:
|
||||
return "EGL_BAD_PARAMETER"
|
||||
case C.EGL_BAD_NATIVE_PIXMAP:
|
||||
return "EGL_BAD_NATIVE_PIXMAP"
|
||||
case C.EGL_BAD_NATIVE_WINDOW:
|
||||
return "EGL_BAD_NATIVE_WINDOW"
|
||||
case C.EGL_CONTEXT_LOST:
|
||||
return "EGL_CONTEXT_LOST"
|
||||
default:
|
||||
return fmt.Sprintf("Unknown EGL err: %d", errNum)
|
||||
}
|
||||
}
|
||||
|
||||
func convAndroidKeyCode(aKeyCode int32) key.Code {
|
||||
// Many Android key codes do not map into USB HID codes.
|
||||
// For those, key.CodeUnknown is returned. This switch has all
|
||||
// cases, even the unknown ones, to serve as a documentation
|
||||
// and search aid.
|
||||
switch aKeyCode {
|
||||
case C.AKEYCODE_UNKNOWN:
|
||||
case C.AKEYCODE_SOFT_LEFT:
|
||||
case C.AKEYCODE_SOFT_RIGHT:
|
||||
case C.AKEYCODE_HOME:
|
||||
return key.CodeHome
|
||||
case C.AKEYCODE_BACK:
|
||||
case C.AKEYCODE_CALL:
|
||||
case C.AKEYCODE_ENDCALL:
|
||||
case C.AKEYCODE_0:
|
||||
return key.Code0
|
||||
case C.AKEYCODE_1:
|
||||
return key.Code1
|
||||
case C.AKEYCODE_2:
|
||||
return key.Code2
|
||||
case C.AKEYCODE_3:
|
||||
return key.Code3
|
||||
case C.AKEYCODE_4:
|
||||
return key.Code4
|
||||
case C.AKEYCODE_5:
|
||||
return key.Code5
|
||||
case C.AKEYCODE_6:
|
||||
return key.Code6
|
||||
case C.AKEYCODE_7:
|
||||
return key.Code7
|
||||
case C.AKEYCODE_8:
|
||||
return key.Code8
|
||||
case C.AKEYCODE_9:
|
||||
return key.Code9
|
||||
case C.AKEYCODE_STAR:
|
||||
case C.AKEYCODE_POUND:
|
||||
case C.AKEYCODE_DPAD_UP:
|
||||
case C.AKEYCODE_DPAD_DOWN:
|
||||
case C.AKEYCODE_DPAD_LEFT:
|
||||
case C.AKEYCODE_DPAD_RIGHT:
|
||||
case C.AKEYCODE_DPAD_CENTER:
|
||||
case C.AKEYCODE_VOLUME_UP:
|
||||
return key.CodeVolumeUp
|
||||
case C.AKEYCODE_VOLUME_DOWN:
|
||||
return key.CodeVolumeDown
|
||||
case C.AKEYCODE_POWER:
|
||||
case C.AKEYCODE_CAMERA:
|
||||
case C.AKEYCODE_CLEAR:
|
||||
case C.AKEYCODE_A:
|
||||
return key.CodeA
|
||||
case C.AKEYCODE_B:
|
||||
return key.CodeB
|
||||
case C.AKEYCODE_C:
|
||||
return key.CodeC
|
||||
case C.AKEYCODE_D:
|
||||
return key.CodeD
|
||||
case C.AKEYCODE_E:
|
||||
return key.CodeE
|
||||
case C.AKEYCODE_F:
|
||||
return key.CodeF
|
||||
case C.AKEYCODE_G:
|
||||
return key.CodeG
|
||||
case C.AKEYCODE_H:
|
||||
return key.CodeH
|
||||
case C.AKEYCODE_I:
|
||||
return key.CodeI
|
||||
case C.AKEYCODE_J:
|
||||
return key.CodeJ
|
||||
case C.AKEYCODE_K:
|
||||
return key.CodeK
|
||||
case C.AKEYCODE_L:
|
||||
return key.CodeL
|
||||
case C.AKEYCODE_M:
|
||||
return key.CodeM
|
||||
case C.AKEYCODE_N:
|
||||
return key.CodeN
|
||||
case C.AKEYCODE_O:
|
||||
return key.CodeO
|
||||
case C.AKEYCODE_P:
|
||||
return key.CodeP
|
||||
case C.AKEYCODE_Q:
|
||||
return key.CodeQ
|
||||
case C.AKEYCODE_R:
|
||||
return key.CodeR
|
||||
case C.AKEYCODE_S:
|
||||
return key.CodeS
|
||||
case C.AKEYCODE_T:
|
||||
return key.CodeT
|
||||
case C.AKEYCODE_U:
|
||||
return key.CodeU
|
||||
case C.AKEYCODE_V:
|
||||
return key.CodeV
|
||||
case C.AKEYCODE_W:
|
||||
return key.CodeW
|
||||
case C.AKEYCODE_X:
|
||||
return key.CodeX
|
||||
case C.AKEYCODE_Y:
|
||||
return key.CodeY
|
||||
case C.AKEYCODE_Z:
|
||||
return key.CodeZ
|
||||
case C.AKEYCODE_COMMA:
|
||||
return key.CodeComma
|
||||
case C.AKEYCODE_PERIOD:
|
||||
return key.CodeFullStop
|
||||
case C.AKEYCODE_ALT_LEFT:
|
||||
return key.CodeLeftAlt
|
||||
case C.AKEYCODE_ALT_RIGHT:
|
||||
return key.CodeRightAlt
|
||||
case C.AKEYCODE_SHIFT_LEFT:
|
||||
return key.CodeLeftShift
|
||||
case C.AKEYCODE_SHIFT_RIGHT:
|
||||
return key.CodeRightShift
|
||||
case C.AKEYCODE_TAB:
|
||||
return key.CodeTab
|
||||
case C.AKEYCODE_SPACE:
|
||||
return key.CodeSpacebar
|
||||
case C.AKEYCODE_SYM:
|
||||
case C.AKEYCODE_EXPLORER:
|
||||
case C.AKEYCODE_ENVELOPE:
|
||||
case C.AKEYCODE_ENTER:
|
||||
return key.CodeReturnEnter
|
||||
case C.AKEYCODE_DEL:
|
||||
return key.CodeDeleteBackspace
|
||||
case C.AKEYCODE_GRAVE:
|
||||
return key.CodeGraveAccent
|
||||
case C.AKEYCODE_MINUS:
|
||||
return key.CodeHyphenMinus
|
||||
case C.AKEYCODE_EQUALS:
|
||||
return key.CodeEqualSign
|
||||
case C.AKEYCODE_LEFT_BRACKET:
|
||||
return key.CodeLeftSquareBracket
|
||||
case C.AKEYCODE_RIGHT_BRACKET:
|
||||
return key.CodeRightSquareBracket
|
||||
case C.AKEYCODE_BACKSLASH:
|
||||
return key.CodeBackslash
|
||||
case C.AKEYCODE_SEMICOLON:
|
||||
return key.CodeSemicolon
|
||||
case C.AKEYCODE_APOSTROPHE:
|
||||
return key.CodeApostrophe
|
||||
case C.AKEYCODE_SLASH:
|
||||
return key.CodeSlash
|
||||
case C.AKEYCODE_AT:
|
||||
case C.AKEYCODE_NUM:
|
||||
case C.AKEYCODE_HEADSETHOOK:
|
||||
case C.AKEYCODE_FOCUS:
|
||||
case C.AKEYCODE_PLUS:
|
||||
case C.AKEYCODE_MENU:
|
||||
case C.AKEYCODE_NOTIFICATION:
|
||||
case C.AKEYCODE_SEARCH:
|
||||
case C.AKEYCODE_MEDIA_PLAY_PAUSE:
|
||||
case C.AKEYCODE_MEDIA_STOP:
|
||||
case C.AKEYCODE_MEDIA_NEXT:
|
||||
case C.AKEYCODE_MEDIA_PREVIOUS:
|
||||
case C.AKEYCODE_MEDIA_REWIND:
|
||||
case C.AKEYCODE_MEDIA_FAST_FORWARD:
|
||||
case C.AKEYCODE_MUTE:
|
||||
case C.AKEYCODE_PAGE_UP:
|
||||
return key.CodePageUp
|
||||
case C.AKEYCODE_PAGE_DOWN:
|
||||
return key.CodePageDown
|
||||
case C.AKEYCODE_PICTSYMBOLS:
|
||||
case C.AKEYCODE_SWITCH_CHARSET:
|
||||
case C.AKEYCODE_BUTTON_A:
|
||||
case C.AKEYCODE_BUTTON_B:
|
||||
case C.AKEYCODE_BUTTON_C:
|
||||
case C.AKEYCODE_BUTTON_X:
|
||||
case C.AKEYCODE_BUTTON_Y:
|
||||
case C.AKEYCODE_BUTTON_Z:
|
||||
case C.AKEYCODE_BUTTON_L1:
|
||||
case C.AKEYCODE_BUTTON_R1:
|
||||
case C.AKEYCODE_BUTTON_L2:
|
||||
case C.AKEYCODE_BUTTON_R2:
|
||||
case C.AKEYCODE_BUTTON_THUMBL:
|
||||
case C.AKEYCODE_BUTTON_THUMBR:
|
||||
case C.AKEYCODE_BUTTON_START:
|
||||
case C.AKEYCODE_BUTTON_SELECT:
|
||||
case C.AKEYCODE_BUTTON_MODE:
|
||||
case C.AKEYCODE_ESCAPE:
|
||||
return key.CodeEscape
|
||||
case C.AKEYCODE_FORWARD_DEL:
|
||||
return key.CodeDeleteForward
|
||||
case C.AKEYCODE_CTRL_LEFT:
|
||||
return key.CodeLeftControl
|
||||
case C.AKEYCODE_CTRL_RIGHT:
|
||||
return key.CodeRightControl
|
||||
case C.AKEYCODE_CAPS_LOCK:
|
||||
return key.CodeCapsLock
|
||||
case C.AKEYCODE_SCROLL_LOCK:
|
||||
case C.AKEYCODE_META_LEFT:
|
||||
return key.CodeLeftGUI
|
||||
case C.AKEYCODE_META_RIGHT:
|
||||
return key.CodeRightGUI
|
||||
case C.AKEYCODE_FUNCTION:
|
||||
case C.AKEYCODE_SYSRQ:
|
||||
case C.AKEYCODE_BREAK:
|
||||
case C.AKEYCODE_MOVE_HOME:
|
||||
case C.AKEYCODE_MOVE_END:
|
||||
case C.AKEYCODE_INSERT:
|
||||
return key.CodeInsert
|
||||
case C.AKEYCODE_FORWARD:
|
||||
case C.AKEYCODE_MEDIA_PLAY:
|
||||
case C.AKEYCODE_MEDIA_PAUSE:
|
||||
case C.AKEYCODE_MEDIA_CLOSE:
|
||||
case C.AKEYCODE_MEDIA_EJECT:
|
||||
case C.AKEYCODE_MEDIA_RECORD:
|
||||
case C.AKEYCODE_F1:
|
||||
return key.CodeF1
|
||||
case C.AKEYCODE_F2:
|
||||
return key.CodeF2
|
||||
case C.AKEYCODE_F3:
|
||||
return key.CodeF3
|
||||
case C.AKEYCODE_F4:
|
||||
return key.CodeF4
|
||||
case C.AKEYCODE_F5:
|
||||
return key.CodeF5
|
||||
case C.AKEYCODE_F6:
|
||||
return key.CodeF6
|
||||
case C.AKEYCODE_F7:
|
||||
return key.CodeF7
|
||||
case C.AKEYCODE_F8:
|
||||
return key.CodeF8
|
||||
case C.AKEYCODE_F9:
|
||||
return key.CodeF9
|
||||
case C.AKEYCODE_F10:
|
||||
return key.CodeF10
|
||||
case C.AKEYCODE_F11:
|
||||
return key.CodeF11
|
||||
case C.AKEYCODE_F12:
|
||||
return key.CodeF12
|
||||
case C.AKEYCODE_NUM_LOCK:
|
||||
return key.CodeKeypadNumLock
|
||||
case C.AKEYCODE_NUMPAD_0:
|
||||
return key.CodeKeypad0
|
||||
case C.AKEYCODE_NUMPAD_1:
|
||||
return key.CodeKeypad1
|
||||
case C.AKEYCODE_NUMPAD_2:
|
||||
return key.CodeKeypad2
|
||||
case C.AKEYCODE_NUMPAD_3:
|
||||
return key.CodeKeypad3
|
||||
case C.AKEYCODE_NUMPAD_4:
|
||||
return key.CodeKeypad4
|
||||
case C.AKEYCODE_NUMPAD_5:
|
||||
return key.CodeKeypad5
|
||||
case C.AKEYCODE_NUMPAD_6:
|
||||
return key.CodeKeypad6
|
||||
case C.AKEYCODE_NUMPAD_7:
|
||||
return key.CodeKeypad7
|
||||
case C.AKEYCODE_NUMPAD_8:
|
||||
return key.CodeKeypad8
|
||||
case C.AKEYCODE_NUMPAD_9:
|
||||
return key.CodeKeypad9
|
||||
case C.AKEYCODE_NUMPAD_DIVIDE:
|
||||
return key.CodeKeypadSlash
|
||||
case C.AKEYCODE_NUMPAD_MULTIPLY:
|
||||
return key.CodeKeypadAsterisk
|
||||
case C.AKEYCODE_NUMPAD_SUBTRACT:
|
||||
return key.CodeKeypadHyphenMinus
|
||||
case C.AKEYCODE_NUMPAD_ADD:
|
||||
return key.CodeKeypadPlusSign
|
||||
case C.AKEYCODE_NUMPAD_DOT:
|
||||
return key.CodeKeypadFullStop
|
||||
case C.AKEYCODE_NUMPAD_COMMA:
|
||||
case C.AKEYCODE_NUMPAD_ENTER:
|
||||
return key.CodeKeypadEnter
|
||||
case C.AKEYCODE_NUMPAD_EQUALS:
|
||||
return key.CodeKeypadEqualSign
|
||||
case C.AKEYCODE_NUMPAD_LEFT_PAREN:
|
||||
case C.AKEYCODE_NUMPAD_RIGHT_PAREN:
|
||||
case C.AKEYCODE_VOLUME_MUTE:
|
||||
return key.CodeMute
|
||||
case C.AKEYCODE_INFO:
|
||||
case C.AKEYCODE_CHANNEL_UP:
|
||||
case C.AKEYCODE_CHANNEL_DOWN:
|
||||
case C.AKEYCODE_ZOOM_IN:
|
||||
case C.AKEYCODE_ZOOM_OUT:
|
||||
case C.AKEYCODE_TV:
|
||||
case C.AKEYCODE_WINDOW:
|
||||
case C.AKEYCODE_GUIDE:
|
||||
case C.AKEYCODE_DVR:
|
||||
case C.AKEYCODE_BOOKMARK:
|
||||
case C.AKEYCODE_CAPTIONS:
|
||||
case C.AKEYCODE_SETTINGS:
|
||||
case C.AKEYCODE_TV_POWER:
|
||||
case C.AKEYCODE_TV_INPUT:
|
||||
case C.AKEYCODE_STB_POWER:
|
||||
case C.AKEYCODE_STB_INPUT:
|
||||
case C.AKEYCODE_AVR_POWER:
|
||||
case C.AKEYCODE_AVR_INPUT:
|
||||
case C.AKEYCODE_PROG_RED:
|
||||
case C.AKEYCODE_PROG_GREEN:
|
||||
case C.AKEYCODE_PROG_YELLOW:
|
||||
case C.AKEYCODE_PROG_BLUE:
|
||||
case C.AKEYCODE_APP_SWITCH:
|
||||
case C.AKEYCODE_BUTTON_1:
|
||||
case C.AKEYCODE_BUTTON_2:
|
||||
case C.AKEYCODE_BUTTON_3:
|
||||
case C.AKEYCODE_BUTTON_4:
|
||||
case C.AKEYCODE_BUTTON_5:
|
||||
case C.AKEYCODE_BUTTON_6:
|
||||
case C.AKEYCODE_BUTTON_7:
|
||||
case C.AKEYCODE_BUTTON_8:
|
||||
case C.AKEYCODE_BUTTON_9:
|
||||
case C.AKEYCODE_BUTTON_10:
|
||||
case C.AKEYCODE_BUTTON_11:
|
||||
case C.AKEYCODE_BUTTON_12:
|
||||
case C.AKEYCODE_BUTTON_13:
|
||||
case C.AKEYCODE_BUTTON_14:
|
||||
case C.AKEYCODE_BUTTON_15:
|
||||
case C.AKEYCODE_BUTTON_16:
|
||||
case C.AKEYCODE_LANGUAGE_SWITCH:
|
||||
case C.AKEYCODE_MANNER_MODE:
|
||||
case C.AKEYCODE_3D_MODE:
|
||||
case C.AKEYCODE_CONTACTS:
|
||||
case C.AKEYCODE_CALENDAR:
|
||||
case C.AKEYCODE_MUSIC:
|
||||
case C.AKEYCODE_CALCULATOR:
|
||||
}
|
||||
/* Defined in an NDK API version beyond what we use today:
|
||||
C.AKEYCODE_ASSIST
|
||||
C.AKEYCODE_BRIGHTNESS_DOWN
|
||||
C.AKEYCODE_BRIGHTNESS_UP
|
||||
C.AKEYCODE_EISU
|
||||
C.AKEYCODE_HENKAN
|
||||
C.AKEYCODE_KANA
|
||||
C.AKEYCODE_KATAKANA_HIRAGANA
|
||||
C.AKEYCODE_MEDIA_AUDIO_TRACK
|
||||
C.AKEYCODE_MUHENKAN
|
||||
C.AKEYCODE_RO
|
||||
C.AKEYCODE_YEN
|
||||
C.AKEYCODE_ZENKAKU_HANKAKU
|
||||
*/
|
||||
return key.CodeUnknown
|
||||
}
|
213
app/app.go
Normal file
213
app/app.go
Normal file
|
@ -0,0 +1,213 @@
|
|||
// Copyright 2014 The Go Authors. All rights reserved.
|
||||
// Use of this source code is governed by a BSD-style
|
||||
// license that can be found in the LICENSE file.
|
||||
|
||||
//go:build linux || darwin || windows
|
||||
|
||||
package app
|
||||
|
||||
import (
|
||||
"golang.org/x/mobile/event/lifecycle"
|
||||
"golang.org/x/mobile/event/size"
|
||||
"golang.org/x/mobile/gl"
|
||||
_ "golang.org/x/mobile/internal/mobileinit"
|
||||
)
|
||||
|
||||
// Main is called by the main.main function to run the mobile application.
|
||||
//
|
||||
// It calls f on the App, in a separate goroutine, as some OS-specific
|
||||
// libraries require being on 'the main thread'.
|
||||
func Main(f func(App)) {
|
||||
main(f)
|
||||
}
|
||||
|
||||
// App is how a GUI mobile application interacts with the OS.
|
||||
type App interface {
|
||||
// Events returns the events channel. It carries events from the system to
|
||||
// the app. The type of such events include:
|
||||
// - lifecycle.Event
|
||||
// - mouse.Event
|
||||
// - paint.Event
|
||||
// - size.Event
|
||||
// - touch.Event
|
||||
// from the golang.org/x/mobile/event/etc packages. Other packages may
|
||||
// define other event types that are carried on this channel.
|
||||
Events() <-chan interface{}
|
||||
|
||||
// Send sends an event on the events channel. It does not block.
|
||||
Send(event interface{})
|
||||
|
||||
// Publish flushes any pending drawing commands, such as OpenGL calls, and
|
||||
// swaps the back buffer to the screen.
|
||||
Publish() PublishResult
|
||||
|
||||
// TODO: replace filters (and the Events channel) with a NextEvent method?
|
||||
|
||||
// Filter calls each registered event filter function in sequence.
|
||||
Filter(event interface{}) interface{}
|
||||
|
||||
// RegisterFilter registers a event filter function to be called by Filter. The
|
||||
// function can return a different event, or return nil to consume the event,
|
||||
// but the function can also return its argument unchanged, where its purpose
|
||||
// is to trigger a side effect rather than modify the event.
|
||||
RegisterFilter(f func(interface{}) interface{})
|
||||
}
|
||||
|
||||
// PublishResult is the result of an App.Publish call.
|
||||
type PublishResult struct {
|
||||
// BackBufferPreserved is whether the contents of the back buffer was
|
||||
// preserved. If false, the contents are undefined.
|
||||
BackBufferPreserved bool
|
||||
}
|
||||
|
||||
var theApp = &app{
|
||||
eventsOut: make(chan interface{}),
|
||||
lifecycleStage: lifecycle.StageDead,
|
||||
publish: make(chan struct{}),
|
||||
publishResult: make(chan PublishResult),
|
||||
}
|
||||
|
||||
func init() {
|
||||
theApp.eventsIn = pump(theApp.eventsOut)
|
||||
theApp.glctx, theApp.worker = gl.NewContext()
|
||||
}
|
||||
|
||||
func (a *app) sendLifecycle(to lifecycle.Stage) {
|
||||
if a.lifecycleStage == to {
|
||||
return
|
||||
}
|
||||
a.eventsIn <- lifecycle.Event{
|
||||
From: a.lifecycleStage,
|
||||
To: to,
|
||||
DrawContext: a.glctx,
|
||||
}
|
||||
a.lifecycleStage = to
|
||||
}
|
||||
|
||||
type app struct {
|
||||
filters []func(interface{}) interface{}
|
||||
|
||||
eventsOut chan interface{}
|
||||
eventsIn chan interface{}
|
||||
lifecycleStage lifecycle.Stage
|
||||
publish chan struct{}
|
||||
publishResult chan PublishResult
|
||||
|
||||
glctx gl.Context
|
||||
worker gl.Worker
|
||||
}
|
||||
|
||||
func (a *app) Events() <-chan interface{} {
|
||||
return a.eventsOut
|
||||
}
|
||||
|
||||
func (a *app) Send(event interface{}) {
|
||||
a.eventsIn <- event
|
||||
}
|
||||
|
||||
func (a *app) Publish() PublishResult {
|
||||
// gl.Flush is a lightweight (on modern GL drivers) blocking call
|
||||
// that ensures all GL functions pending in the gl package have
|
||||
// been passed onto the GL driver before the app package attempts
|
||||
// to swap the screen buffer.
|
||||
//
|
||||
// This enforces that the final receive (for this paint cycle) on
|
||||
// gl.WorkAvailable happens before the send on endPaint.
|
||||
a.glctx.Flush()
|
||||
a.publish <- struct{}{}
|
||||
return <-a.publishResult
|
||||
}
|
||||
|
||||
func (a *app) Filter(event interface{}) interface{} {
|
||||
for _, f := range a.filters {
|
||||
event = f(event)
|
||||
}
|
||||
return event
|
||||
}
|
||||
|
||||
func (a *app) RegisterFilter(f func(interface{}) interface{}) {
|
||||
a.filters = append(a.filters, f)
|
||||
}
|
||||
|
||||
type stopPumping struct{}
|
||||
|
||||
// pump returns a channel src such that sending on src will eventually send on
|
||||
// dst, in order, but that src will always be ready to send/receive soon, even
|
||||
// if dst currently isn't. It is effectively an infinitely buffered channel.
|
||||
//
|
||||
// In particular, goroutine A sending on src will not deadlock even if goroutine
|
||||
// B that's responsible for receiving on dst is currently blocked trying to
|
||||
// send to A on a separate channel.
|
||||
//
|
||||
// Send a stopPumping on the src channel to close the dst channel after all queued
|
||||
// events are sent on dst. After that, other goroutines can still send to src,
|
||||
// so that such sends won't block forever, but such events will be ignored.
|
||||
func pump(dst chan interface{}) (src chan interface{}) {
|
||||
src = make(chan interface{})
|
||||
go func() {
|
||||
// initialSize is the initial size of the circular buffer. It must be a
|
||||
// power of 2.
|
||||
const initialSize = 16
|
||||
i, j, buf, mask := 0, 0, make([]interface{}, initialSize), initialSize-1
|
||||
|
||||
srcActive := true
|
||||
for {
|
||||
maybeDst := dst
|
||||
if i == j {
|
||||
maybeDst = nil
|
||||
}
|
||||
if maybeDst == nil && !srcActive {
|
||||
// Pump is stopped and empty.
|
||||
break
|
||||
}
|
||||
|
||||
select {
|
||||
case maybeDst <- buf[i&mask]:
|
||||
buf[i&mask] = nil
|
||||
i++
|
||||
|
||||
case e := <-src:
|
||||
if _, ok := e.(stopPumping); ok {
|
||||
srcActive = false
|
||||
continue
|
||||
}
|
||||
|
||||
if !srcActive {
|
||||
continue
|
||||
}
|
||||
|
||||
// Allocate a bigger buffer if necessary.
|
||||
if i+len(buf) == j {
|
||||
b := make([]interface{}, 2*len(buf))
|
||||
n := copy(b, buf[j&mask:])
|
||||
copy(b[n:], buf[:j&mask])
|
||||
i, j = 0, len(buf)
|
||||
buf, mask = b, len(b)-1
|
||||
}
|
||||
|
||||
buf[j&mask] = e
|
||||
j++
|
||||
}
|
||||
}
|
||||
|
||||
close(dst)
|
||||
// Block forever.
|
||||
for range src {
|
||||
}
|
||||
}()
|
||||
return src
|
||||
}
|
||||
|
||||
// TODO: do this for all build targets, not just linux (x11 and Android)? If
|
||||
// so, should package gl instead of this package call RegisterFilter??
|
||||
//
|
||||
// TODO: does Android need this?? It seems to work without it (Nexus 7,
|
||||
// KitKat). If only x11 needs this, should we move this to x11.go??
|
||||
func (a *app) registerGLViewportFilter() {
|
||||
a.RegisterFilter(func(e interface{}) interface{} {
|
||||
if e, ok := e.(size.Event); ok {
|
||||
a.glctx.Viewport(0, 0, e.WidthPx, e.HeightPx)
|
||||
}
|
||||
return e
|
||||
})
|
||||
}
|
237
app/app_test.go
Normal file
237
app/app_test.go
Normal file
|
@ -0,0 +1,237 @@
|
|||
// Copyright 2015 The Go Authors. All rights reserved.
|
||||
// Use of this source code is governed by a BSD-style
|
||||
// license that can be found in the LICENSE file.
|
||||
|
||||
package app_test
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"image"
|
||||
"image/color"
|
||||
_ "image/png"
|
||||
"net"
|
||||
"os"
|
||||
"os/exec"
|
||||
"strings"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"golang.org/x/mobile/app/internal/apptest"
|
||||
"golang.org/x/mobile/event/size"
|
||||
)
|
||||
|
||||
// TestAndroidApp tests the lifecycle, event, and window semantics of a
|
||||
// simple android app.
|
||||
//
|
||||
// Beyond testing the app package, the goal is to eventually have
|
||||
// helper libraries that make tests like these easy to write. Hopefully
|
||||
// having a user of such a fictional package will help illuminate the way.
|
||||
func TestAndroidApp(t *testing.T) {
|
||||
t.Skip("see issue #23835")
|
||||
if _, err := exec.Command("which", "adb").CombinedOutput(); err != nil {
|
||||
t.Skip("command adb not found, skipping")
|
||||
}
|
||||
devicesTxt, err := exec.Command("adb", "devices").CombinedOutput()
|
||||
if err != nil {
|
||||
t.Errorf("adb devices failed: %v: %v", err, devicesTxt)
|
||||
}
|
||||
deviceCount := 0
|
||||
for _, d := range strings.Split(strings.TrimSpace(string(devicesTxt)), "\n") {
|
||||
if strings.Contains(d, "List of devices") {
|
||||
continue
|
||||
}
|
||||
// TODO(crawshaw): I believe some unusable devices can appear in the
|
||||
// list with note on them, but I cannot reproduce this right now.
|
||||
deviceCount++
|
||||
}
|
||||
if deviceCount == 0 {
|
||||
t.Skip("no android devices attached")
|
||||
}
|
||||
|
||||
run(t, "gomobile", "version")
|
||||
|
||||
origWD, err := os.Getwd()
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
tmpdir, err := os.MkdirTemp("", "app-test-")
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
defer os.RemoveAll(tmpdir)
|
||||
|
||||
if err := os.Chdir(tmpdir); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
defer os.Chdir(origWD)
|
||||
|
||||
run(t, "gomobile", "install", "golang.org/x/mobile/app/internal/testapp")
|
||||
|
||||
ln, err := net.Listen("tcp4", "localhost:0")
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
defer ln.Close()
|
||||
localaddr := fmt.Sprintf("tcp:%d", ln.Addr().(*net.TCPAddr).Port)
|
||||
t.Logf("local address: %s", localaddr)
|
||||
|
||||
exec.Command("adb", "reverse", "--remove", "tcp:"+apptest.Port).Run() // ignore failure
|
||||
run(t, "adb", "reverse", "tcp:"+apptest.Port, localaddr)
|
||||
|
||||
const (
|
||||
KeycodePower = "26"
|
||||
KeycodeUnlock = "82"
|
||||
)
|
||||
|
||||
run(t, "adb", "shell", "input", "keyevent", KeycodePower)
|
||||
run(t, "adb", "shell", "input", "keyevent", KeycodeUnlock)
|
||||
|
||||
const (
|
||||
rotationPortrait = "0"
|
||||
rotationLandscape = "1"
|
||||
)
|
||||
|
||||
rotate := func(rotation string) {
|
||||
run(t, "adb", "shell", "content", "insert", "--uri", "content://settings/system", "--bind", "name:s:user_rotation", "--bind", "value:i:"+rotation)
|
||||
}
|
||||
|
||||
// turn off automatic rotation and start in portrait
|
||||
run(t, "adb", "shell", "content", "insert", "--uri", "content://settings/system", "--bind", "name:s:accelerometer_rotation", "--bind", "value:i:0")
|
||||
rotate(rotationPortrait)
|
||||
|
||||
// start testapp
|
||||
run(t,
|
||||
"adb", "shell", "am", "start", "-n",
|
||||
"org.golang.testapp/org.golang.app.GoNativeActivity",
|
||||
)
|
||||
|
||||
var conn net.Conn
|
||||
connDone := make(chan struct{})
|
||||
go func() {
|
||||
conn, err = ln.Accept()
|
||||
connDone <- struct{}{}
|
||||
}()
|
||||
|
||||
select {
|
||||
case <-time.After(5 * time.Second):
|
||||
t.Fatal("timeout waiting for testapp to dial host")
|
||||
case <-connDone:
|
||||
if err != nil {
|
||||
t.Fatalf("ln.Accept: %v", err)
|
||||
}
|
||||
}
|
||||
defer conn.Close()
|
||||
comm := &apptest.Comm{
|
||||
Conn: conn,
|
||||
Fatalf: t.Fatalf,
|
||||
Printf: t.Logf,
|
||||
}
|
||||
|
||||
var pixelsPerPt float32
|
||||
var orientation size.Orientation
|
||||
|
||||
comm.Recv("hello_from_testapp")
|
||||
comm.Send("hello_from_host")
|
||||
comm.Recv("lifecycle_visible")
|
||||
comm.Recv("size", &pixelsPerPt, &orientation)
|
||||
if pixelsPerPt < 0.1 {
|
||||
t.Fatalf("bad pixelsPerPt: %f", pixelsPerPt)
|
||||
}
|
||||
|
||||
// A single paint event is sent when the lifecycle enters
|
||||
// StageVisible, and after the end of a touch event.
|
||||
var color string
|
||||
comm.Recv("paint", &color)
|
||||
// Ignore the first paint color, it may be slow making it to the screen.
|
||||
|
||||
rotate(rotationLandscape)
|
||||
comm.Recv("size", &pixelsPerPt, &orientation)
|
||||
if want := size.OrientationLandscape; orientation != want {
|
||||
t.Errorf("want orientation %d, got %d", want, orientation)
|
||||
}
|
||||
|
||||
var x, y int
|
||||
var ty string
|
||||
|
||||
tap(t, 50, 260)
|
||||
comm.Recv("touch", &ty, &x, &y)
|
||||
if ty != "begin" || x != 50 || y != 260 {
|
||||
t.Errorf("want touch begin(50, 260), got %s(%d,%d)", ty, x, y)
|
||||
}
|
||||
comm.Recv("touch", &ty, &x, &y)
|
||||
if ty != "end" || x != 50 || y != 260 {
|
||||
t.Errorf("want touch end(50, 260), got %s(%d,%d)", ty, x, y)
|
||||
}
|
||||
|
||||
comm.Recv("paint", &color)
|
||||
if gotColor := currentColor(t); color != gotColor {
|
||||
t.Errorf("app reports color %q, but saw %q", color, gotColor)
|
||||
}
|
||||
|
||||
rotate(rotationPortrait)
|
||||
comm.Recv("size", &pixelsPerPt, &orientation)
|
||||
if want := size.OrientationPortrait; orientation != want {
|
||||
t.Errorf("want orientation %d, got %d", want, orientation)
|
||||
}
|
||||
|
||||
tap(t, 50, 260)
|
||||
comm.Recv("touch", &ty, &x, &y) // touch begin
|
||||
comm.Recv("touch", &ty, &x, &y) // touch end
|
||||
comm.Recv("paint", &color)
|
||||
if gotColor := currentColor(t); color != gotColor {
|
||||
t.Errorf("app reports color %q, but saw %q", color, gotColor)
|
||||
}
|
||||
|
||||
// TODO: lifecycle testing (NOTE: adb shell input keyevent 4 is the back button)
|
||||
}
|
||||
|
||||
func currentColor(t *testing.T) string {
|
||||
file := fmt.Sprintf("app-screen-%d.png", time.Now().Unix())
|
||||
|
||||
run(t, "adb", "shell", "screencap", "-p", "/data/local/tmp/"+file)
|
||||
run(t, "adb", "pull", "/data/local/tmp/"+file)
|
||||
run(t, "adb", "shell", "rm", "/data/local/tmp/"+file)
|
||||
defer os.Remove(file)
|
||||
|
||||
f, err := os.Open(file)
|
||||
if err != nil {
|
||||
t.Errorf("currentColor: cannot open screencap: %v", err)
|
||||
return ""
|
||||
}
|
||||
m, _, err := image.Decode(f)
|
||||
if err != nil {
|
||||
t.Errorf("currentColor: cannot decode screencap: %v", err)
|
||||
return ""
|
||||
}
|
||||
var center color.Color
|
||||
{
|
||||
b := m.Bounds()
|
||||
x, y := b.Min.X+(b.Max.X-b.Min.X)/2, b.Min.Y+(b.Max.Y-b.Min.Y)/2
|
||||
center = m.At(x, y)
|
||||
}
|
||||
r, g, b, _ := center.RGBA()
|
||||
switch {
|
||||
case r == 0xffff && g == 0x0000 && b == 0x0000:
|
||||
return "red"
|
||||
case r == 0x0000 && g == 0xffff && b == 0x0000:
|
||||
return "green"
|
||||
case r == 0x0000 && g == 0x0000 && b == 0xffff:
|
||||
return "blue"
|
||||
default:
|
||||
return fmt.Sprintf("indeterminate: %v", center)
|
||||
}
|
||||
}
|
||||
|
||||
func tap(t *testing.T, x, y int) {
|
||||
run(t, "adb", "shell", "input", "tap", fmt.Sprintf("%d", x), fmt.Sprintf("%d", y))
|
||||
}
|
||||
|
||||
func run(t *testing.T, cmdName string, arg ...string) {
|
||||
cmd := exec.Command(cmdName, arg...)
|
||||
t.Log(strings.Join(cmd.Args, " "))
|
||||
out, err := cmd.CombinedOutput()
|
||||
if err != nil {
|
||||
t.Fatalf("%s %v: %s", strings.Join(cmd.Args, " "), err, out)
|
||||
}
|
||||
}
|
495
app/darwin_desktop.go
Normal file
495
app/darwin_desktop.go
Normal file
|
@ -0,0 +1,495 @@
|
|||
// Copyright 2014 The Go Authors. All rights reserved.
|
||||
// Use of this source code is governed by a BSD-style
|
||||
// license that can be found in the LICENSE file.
|
||||
|
||||
//go:build darwin && !ios
|
||||
|
||||
package app
|
||||
|
||||
// Simple on-screen app debugging for OS X. Not an officially supported
|
||||
// development target for apps, as screens with mice are very different
|
||||
// than screens with touch panels.
|
||||
|
||||
/*
|
||||
#cgo CFLAGS: -x objective-c -DGL_SILENCE_DEPRECATION
|
||||
#cgo LDFLAGS: -framework Cocoa -framework OpenGL
|
||||
#import <Carbon/Carbon.h> // for HIToolbox/Events.h
|
||||
#import <Cocoa/Cocoa.h>
|
||||
#include <pthread.h>
|
||||
|
||||
void runApp(void);
|
||||
void stopApp(void);
|
||||
void makeCurrentContext(GLintptr);
|
||||
uint64 threadID();
|
||||
*/
|
||||
import "C"
|
||||
import (
|
||||
"log"
|
||||
"runtime"
|
||||
"sync"
|
||||
|
||||
"golang.org/x/mobile/event/key"
|
||||
"golang.org/x/mobile/event/lifecycle"
|
||||
"golang.org/x/mobile/event/paint"
|
||||
"golang.org/x/mobile/event/size"
|
||||
"golang.org/x/mobile/event/touch"
|
||||
"golang.org/x/mobile/geom"
|
||||
)
|
||||
|
||||
var initThreadID uint64
|
||||
|
||||
func init() {
|
||||
// Lock the goroutine responsible for initialization to an OS thread.
|
||||
// This means the goroutine running main (and calling runApp below)
|
||||
// is locked to the OS thread that started the program. This is
|
||||
// necessary for the correct delivery of Cocoa events to the process.
|
||||
//
|
||||
// A discussion on this topic:
|
||||
// https://groups.google.com/forum/#!msg/golang-nuts/IiWZ2hUuLDA/SNKYYZBelsYJ
|
||||
runtime.LockOSThread()
|
||||
initThreadID = uint64(C.threadID())
|
||||
}
|
||||
|
||||
func main(f func(App)) {
|
||||
if tid := uint64(C.threadID()); tid != initThreadID {
|
||||
log.Fatalf("app.Main called on thread %d, but app.init ran on %d", tid, initThreadID)
|
||||
}
|
||||
|
||||
go func() {
|
||||
f(theApp)
|
||||
C.stopApp()
|
||||
}()
|
||||
|
||||
C.runApp()
|
||||
}
|
||||
|
||||
// loop is the primary drawing loop.
|
||||
//
|
||||
// After Cocoa has captured the initial OS thread for processing Cocoa
|
||||
// events in runApp, it starts loop on another goroutine. It is locked
|
||||
// to an OS thread for its OpenGL context.
|
||||
//
|
||||
// The loop processes GL calls until a publish event appears.
|
||||
// Then it runs any remaining GL calls and flushes the screen.
|
||||
//
|
||||
// As NSOpenGLCPSwapInterval is set to 1, the call to CGLFlushDrawable
|
||||
// blocks until the screen refresh.
|
||||
func (a *app) loop(ctx C.GLintptr) {
|
||||
runtime.LockOSThread()
|
||||
C.makeCurrentContext(ctx)
|
||||
|
||||
workAvailable := a.worker.WorkAvailable()
|
||||
|
||||
for {
|
||||
select {
|
||||
case <-workAvailable:
|
||||
a.worker.DoWork()
|
||||
case <-theApp.publish:
|
||||
loop1:
|
||||
for {
|
||||
select {
|
||||
case <-workAvailable:
|
||||
a.worker.DoWork()
|
||||
default:
|
||||
break loop1
|
||||
}
|
||||
}
|
||||
C.CGLFlushDrawable(C.CGLGetCurrentContext())
|
||||
theApp.publishResult <- PublishResult{}
|
||||
select {
|
||||
case drawDone <- struct{}{}:
|
||||
default:
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
var drawDone = make(chan struct{})
|
||||
|
||||
// drawgl is used by Cocoa to occasionally request screen updates.
|
||||
//
|
||||
//export drawgl
|
||||
func drawgl() {
|
||||
switch theApp.lifecycleStage {
|
||||
case lifecycle.StageFocused, lifecycle.StageVisible:
|
||||
theApp.Send(paint.Event{
|
||||
External: true,
|
||||
})
|
||||
<-drawDone
|
||||
}
|
||||
}
|
||||
|
||||
//export startloop
|
||||
func startloop(ctx C.GLintptr) {
|
||||
go theApp.loop(ctx)
|
||||
}
|
||||
|
||||
var windowHeightPx float32
|
||||
|
||||
//export setGeom
|
||||
func setGeom(pixelsPerPt float32, widthPx, heightPx int) {
|
||||
windowHeightPx = float32(heightPx)
|
||||
theApp.eventsIn <- size.Event{
|
||||
WidthPx: widthPx,
|
||||
HeightPx: heightPx,
|
||||
WidthPt: geom.Pt(float32(widthPx) / pixelsPerPt),
|
||||
HeightPt: geom.Pt(float32(heightPx) / pixelsPerPt),
|
||||
PixelsPerPt: pixelsPerPt,
|
||||
}
|
||||
}
|
||||
|
||||
var touchEvents struct {
|
||||
sync.Mutex
|
||||
pending []touch.Event
|
||||
}
|
||||
|
||||
func sendTouch(t touch.Type, x, y float32) {
|
||||
theApp.eventsIn <- touch.Event{
|
||||
X: x,
|
||||
Y: windowHeightPx - y,
|
||||
Sequence: 0,
|
||||
Type: t,
|
||||
}
|
||||
}
|
||||
|
||||
//export eventMouseDown
|
||||
func eventMouseDown(x, y float32) { sendTouch(touch.TypeBegin, x, y) }
|
||||
|
||||
//export eventMouseDragged
|
||||
func eventMouseDragged(x, y float32) { sendTouch(touch.TypeMove, x, y) }
|
||||
|
||||
//export eventMouseEnd
|
||||
func eventMouseEnd(x, y float32) { sendTouch(touch.TypeEnd, x, y) }
|
||||
|
||||
//export lifecycleDead
|
||||
func lifecycleDead() { theApp.sendLifecycle(lifecycle.StageDead) }
|
||||
|
||||
//export eventKey
|
||||
func eventKey(runeVal int32, direction uint8, code uint16, flags uint32) {
|
||||
var modifiers key.Modifiers
|
||||
for _, mod := range mods {
|
||||
if flags&mod.flags == mod.flags {
|
||||
modifiers |= mod.mod
|
||||
}
|
||||
}
|
||||
|
||||
theApp.eventsIn <- key.Event{
|
||||
Rune: convRune(rune(runeVal)),
|
||||
Code: convVirtualKeyCode(code),
|
||||
Modifiers: modifiers,
|
||||
Direction: key.Direction(direction),
|
||||
}
|
||||
}
|
||||
|
||||
//export eventFlags
|
||||
func eventFlags(flags uint32) {
|
||||
for _, mod := range mods {
|
||||
if flags&mod.flags == mod.flags && lastFlags&mod.flags != mod.flags {
|
||||
eventKey(-1, uint8(key.DirPress), mod.code, flags)
|
||||
}
|
||||
if lastFlags&mod.flags == mod.flags && flags&mod.flags != mod.flags {
|
||||
eventKey(-1, uint8(key.DirRelease), mod.code, flags)
|
||||
}
|
||||
}
|
||||
lastFlags = flags
|
||||
}
|
||||
|
||||
var lastFlags uint32
|
||||
|
||||
var mods = [...]struct {
|
||||
flags uint32
|
||||
code uint16
|
||||
mod key.Modifiers
|
||||
}{
|
||||
// Left and right variants of modifier keys have their own masks,
|
||||
// but they are not documented. These were determined empirically.
|
||||
{1<<17 | 0x102, C.kVK_Shift, key.ModShift},
|
||||
{1<<17 | 0x104, C.kVK_RightShift, key.ModShift},
|
||||
{1<<18 | 0x101, C.kVK_Control, key.ModControl},
|
||||
// TODO key.ControlRight
|
||||
{1<<19 | 0x120, C.kVK_Option, key.ModAlt},
|
||||
{1<<19 | 0x140, C.kVK_RightOption, key.ModAlt},
|
||||
{1<<20 | 0x108, C.kVK_Command, key.ModMeta},
|
||||
{1<<20 | 0x110, C.kVK_Command, key.ModMeta}, // TODO: missing kVK_RightCommand
|
||||
}
|
||||
|
||||
//export lifecycleAlive
|
||||
func lifecycleAlive() { theApp.sendLifecycle(lifecycle.StageAlive) }
|
||||
|
||||
//export lifecycleVisible
|
||||
func lifecycleVisible() {
|
||||
theApp.sendLifecycle(lifecycle.StageVisible)
|
||||
}
|
||||
|
||||
//export lifecycleFocused
|
||||
func lifecycleFocused() { theApp.sendLifecycle(lifecycle.StageFocused) }
|
||||
|
||||
// convRune marks the Carbon/Cocoa private-range unicode rune representing
|
||||
// a non-unicode key event to -1, used for Rune in the key package.
|
||||
//
|
||||
// http://www.unicode.org/Public/MAPPINGS/VENDORS/APPLE/CORPCHAR.TXT
|
||||
func convRune(r rune) rune {
|
||||
if '\uE000' <= r && r <= '\uF8FF' {
|
||||
return -1
|
||||
}
|
||||
return r
|
||||
}
|
||||
|
||||
// convVirtualKeyCode converts a Carbon/Cocoa virtual key code number
|
||||
// into the standard keycodes used by the key package.
|
||||
//
|
||||
// To get a sense of the key map, see the diagram on
|
||||
//
|
||||
// http://boredzo.org/blog/archives/2007-05-22/virtual-key-codes
|
||||
func convVirtualKeyCode(vkcode uint16) key.Code {
|
||||
switch vkcode {
|
||||
case C.kVK_ANSI_A:
|
||||
return key.CodeA
|
||||
case C.kVK_ANSI_B:
|
||||
return key.CodeB
|
||||
case C.kVK_ANSI_C:
|
||||
return key.CodeC
|
||||
case C.kVK_ANSI_D:
|
||||
return key.CodeD
|
||||
case C.kVK_ANSI_E:
|
||||
return key.CodeE
|
||||
case C.kVK_ANSI_F:
|
||||
return key.CodeF
|
||||
case C.kVK_ANSI_G:
|
||||
return key.CodeG
|
||||
case C.kVK_ANSI_H:
|
||||
return key.CodeH
|
||||
case C.kVK_ANSI_I:
|
||||
return key.CodeI
|
||||
case C.kVK_ANSI_J:
|
||||
return key.CodeJ
|
||||
case C.kVK_ANSI_K:
|
||||
return key.CodeK
|
||||
case C.kVK_ANSI_L:
|
||||
return key.CodeL
|
||||
case C.kVK_ANSI_M:
|
||||
return key.CodeM
|
||||
case C.kVK_ANSI_N:
|
||||
return key.CodeN
|
||||
case C.kVK_ANSI_O:
|
||||
return key.CodeO
|
||||
case C.kVK_ANSI_P:
|
||||
return key.CodeP
|
||||
case C.kVK_ANSI_Q:
|
||||
return key.CodeQ
|
||||
case C.kVK_ANSI_R:
|
||||
return key.CodeR
|
||||
case C.kVK_ANSI_S:
|
||||
return key.CodeS
|
||||
case C.kVK_ANSI_T:
|
||||
return key.CodeT
|
||||
case C.kVK_ANSI_U:
|
||||
return key.CodeU
|
||||
case C.kVK_ANSI_V:
|
||||
return key.CodeV
|
||||
case C.kVK_ANSI_W:
|
||||
return key.CodeW
|
||||
case C.kVK_ANSI_X:
|
||||
return key.CodeX
|
||||
case C.kVK_ANSI_Y:
|
||||
return key.CodeY
|
||||
case C.kVK_ANSI_Z:
|
||||
return key.CodeZ
|
||||
case C.kVK_ANSI_1:
|
||||
return key.Code1
|
||||
case C.kVK_ANSI_2:
|
||||
return key.Code2
|
||||
case C.kVK_ANSI_3:
|
||||
return key.Code3
|
||||
case C.kVK_ANSI_4:
|
||||
return key.Code4
|
||||
case C.kVK_ANSI_5:
|
||||
return key.Code5
|
||||
case C.kVK_ANSI_6:
|
||||
return key.Code6
|
||||
case C.kVK_ANSI_7:
|
||||
return key.Code7
|
||||
case C.kVK_ANSI_8:
|
||||
return key.Code8
|
||||
case C.kVK_ANSI_9:
|
||||
return key.Code9
|
||||
case C.kVK_ANSI_0:
|
||||
return key.Code0
|
||||
// TODO: move the rest of these codes to constants in key.go
|
||||
// if we are happy with them.
|
||||
case C.kVK_Return:
|
||||
return key.CodeReturnEnter
|
||||
case C.kVK_Escape:
|
||||
return key.CodeEscape
|
||||
case C.kVK_Delete:
|
||||
return key.CodeDeleteBackspace
|
||||
case C.kVK_Tab:
|
||||
return key.CodeTab
|
||||
case C.kVK_Space:
|
||||
return key.CodeSpacebar
|
||||
case C.kVK_ANSI_Minus:
|
||||
return key.CodeHyphenMinus
|
||||
case C.kVK_ANSI_Equal:
|
||||
return key.CodeEqualSign
|
||||
case C.kVK_ANSI_LeftBracket:
|
||||
return key.CodeLeftSquareBracket
|
||||
case C.kVK_ANSI_RightBracket:
|
||||
return key.CodeRightSquareBracket
|
||||
case C.kVK_ANSI_Backslash:
|
||||
return key.CodeBackslash
|
||||
// 50: Keyboard Non-US "#" and ~
|
||||
case C.kVK_ANSI_Semicolon:
|
||||
return key.CodeSemicolon
|
||||
case C.kVK_ANSI_Quote:
|
||||
return key.CodeApostrophe
|
||||
case C.kVK_ANSI_Grave:
|
||||
return key.CodeGraveAccent
|
||||
case C.kVK_ANSI_Comma:
|
||||
return key.CodeComma
|
||||
case C.kVK_ANSI_Period:
|
||||
return key.CodeFullStop
|
||||
case C.kVK_ANSI_Slash:
|
||||
return key.CodeSlash
|
||||
case C.kVK_CapsLock:
|
||||
return key.CodeCapsLock
|
||||
case C.kVK_F1:
|
||||
return key.CodeF1
|
||||
case C.kVK_F2:
|
||||
return key.CodeF2
|
||||
case C.kVK_F3:
|
||||
return key.CodeF3
|
||||
case C.kVK_F4:
|
||||
return key.CodeF4
|
||||
case C.kVK_F5:
|
||||
return key.CodeF5
|
||||
case C.kVK_F6:
|
||||
return key.CodeF6
|
||||
case C.kVK_F7:
|
||||
return key.CodeF7
|
||||
case C.kVK_F8:
|
||||
return key.CodeF8
|
||||
case C.kVK_F9:
|
||||
return key.CodeF9
|
||||
case C.kVK_F10:
|
||||
return key.CodeF10
|
||||
case C.kVK_F11:
|
||||
return key.CodeF11
|
||||
case C.kVK_F12:
|
||||
return key.CodeF12
|
||||
// 70: PrintScreen
|
||||
// 71: Scroll Lock
|
||||
// 72: Pause
|
||||
// 73: Insert
|
||||
case C.kVK_Home:
|
||||
return key.CodeHome
|
||||
case C.kVK_PageUp:
|
||||
return key.CodePageUp
|
||||
case C.kVK_ForwardDelete:
|
||||
return key.CodeDeleteForward
|
||||
case C.kVK_End:
|
||||
return key.CodeEnd
|
||||
case C.kVK_PageDown:
|
||||
return key.CodePageDown
|
||||
case C.kVK_RightArrow:
|
||||
return key.CodeRightArrow
|
||||
case C.kVK_LeftArrow:
|
||||
return key.CodeLeftArrow
|
||||
case C.kVK_DownArrow:
|
||||
return key.CodeDownArrow
|
||||
case C.kVK_UpArrow:
|
||||
return key.CodeUpArrow
|
||||
case C.kVK_ANSI_KeypadClear:
|
||||
return key.CodeKeypadNumLock
|
||||
case C.kVK_ANSI_KeypadDivide:
|
||||
return key.CodeKeypadSlash
|
||||
case C.kVK_ANSI_KeypadMultiply:
|
||||
return key.CodeKeypadAsterisk
|
||||
case C.kVK_ANSI_KeypadMinus:
|
||||
return key.CodeKeypadHyphenMinus
|
||||
case C.kVK_ANSI_KeypadPlus:
|
||||
return key.CodeKeypadPlusSign
|
||||
case C.kVK_ANSI_KeypadEnter:
|
||||
return key.CodeKeypadEnter
|
||||
case C.kVK_ANSI_Keypad1:
|
||||
return key.CodeKeypad1
|
||||
case C.kVK_ANSI_Keypad2:
|
||||
return key.CodeKeypad2
|
||||
case C.kVK_ANSI_Keypad3:
|
||||
return key.CodeKeypad3
|
||||
case C.kVK_ANSI_Keypad4:
|
||||
return key.CodeKeypad4
|
||||
case C.kVK_ANSI_Keypad5:
|
||||
return key.CodeKeypad5
|
||||
case C.kVK_ANSI_Keypad6:
|
||||
return key.CodeKeypad6
|
||||
case C.kVK_ANSI_Keypad7:
|
||||
return key.CodeKeypad7
|
||||
case C.kVK_ANSI_Keypad8:
|
||||
return key.CodeKeypad8
|
||||
case C.kVK_ANSI_Keypad9:
|
||||
return key.CodeKeypad9
|
||||
case C.kVK_ANSI_Keypad0:
|
||||
return key.CodeKeypad0
|
||||
case C.kVK_ANSI_KeypadDecimal:
|
||||
return key.CodeKeypadFullStop
|
||||
case C.kVK_ANSI_KeypadEquals:
|
||||
return key.CodeKeypadEqualSign
|
||||
case C.kVK_F13:
|
||||
return key.CodeF13
|
||||
case C.kVK_F14:
|
||||
return key.CodeF14
|
||||
case C.kVK_F15:
|
||||
return key.CodeF15
|
||||
case C.kVK_F16:
|
||||
return key.CodeF16
|
||||
case C.kVK_F17:
|
||||
return key.CodeF17
|
||||
case C.kVK_F18:
|
||||
return key.CodeF18
|
||||
case C.kVK_F19:
|
||||
return key.CodeF19
|
||||
case C.kVK_F20:
|
||||
return key.CodeF20
|
||||
// 116: Keyboard Execute
|
||||
case C.kVK_Help:
|
||||
return key.CodeHelp
|
||||
// 118: Keyboard Menu
|
||||
// 119: Keyboard Select
|
||||
// 120: Keyboard Stop
|
||||
// 121: Keyboard Again
|
||||
// 122: Keyboard Undo
|
||||
// 123: Keyboard Cut
|
||||
// 124: Keyboard Copy
|
||||
// 125: Keyboard Paste
|
||||
// 126: Keyboard Find
|
||||
case C.kVK_Mute:
|
||||
return key.CodeMute
|
||||
case C.kVK_VolumeUp:
|
||||
return key.CodeVolumeUp
|
||||
case C.kVK_VolumeDown:
|
||||
return key.CodeVolumeDown
|
||||
// 130: Keyboard Locking Caps Lock
|
||||
// 131: Keyboard Locking Num Lock
|
||||
// 132: Keyboard Locking Scroll Lock
|
||||
// 133: Keyboard Comma
|
||||
// 134: Keyboard Equal Sign
|
||||
// ...: Bunch of stuff
|
||||
case C.kVK_Control:
|
||||
return key.CodeLeftControl
|
||||
case C.kVK_Shift:
|
||||
return key.CodeLeftShift
|
||||
case C.kVK_Option:
|
||||
return key.CodeLeftAlt
|
||||
case C.kVK_Command:
|
||||
return key.CodeLeftGUI
|
||||
case C.kVK_RightControl:
|
||||
return key.CodeRightControl
|
||||
case C.kVK_RightShift:
|
||||
return key.CodeRightShift
|
||||
case C.kVK_RightOption:
|
||||
return key.CodeRightAlt
|
||||
// TODO key.CodeRightGUI
|
||||
default:
|
||||
return key.CodeUnknown
|
||||
}
|
||||
}
|
251
app/darwin_desktop.m
Normal file
251
app/darwin_desktop.m
Normal file
|
@ -0,0 +1,251 @@
|
|||
// Copyright 2014 The Go Authors. All rights reserved.
|
||||
// Use of this source code is governed by a BSD-style
|
||||
// license that can be found in the LICENSE file.
|
||||
|
||||
//go:build darwin && !ios
|
||||
// +build darwin
|
||||
// +build !ios
|
||||
|
||||
#include "_cgo_export.h"
|
||||
#include <pthread.h>
|
||||
#include <stdio.h>
|
||||
|
||||
#import <Cocoa/Cocoa.h>
|
||||
#import <Foundation/Foundation.h>
|
||||
#import <OpenGL/gl3.h>
|
||||
|
||||
void makeCurrentContext(GLintptr context) {
|
||||
NSOpenGLContext* ctx = (NSOpenGLContext*)context;
|
||||
[ctx makeCurrentContext];
|
||||
}
|
||||
|
||||
uint64 threadID() {
|
||||
uint64 id;
|
||||
if (pthread_threadid_np(pthread_self(), &id)) {
|
||||
abort();
|
||||
}
|
||||
return id;
|
||||
}
|
||||
|
||||
@interface MobileGLView : NSOpenGLView<NSApplicationDelegate, NSWindowDelegate>
|
||||
{
|
||||
}
|
||||
@end
|
||||
|
||||
@implementation MobileGLView
|
||||
- (void)prepareOpenGL {
|
||||
[super prepareOpenGL];
|
||||
[self setWantsBestResolutionOpenGLSurface:YES];
|
||||
GLint swapInt = 1;
|
||||
|
||||
#pragma clang diagnostic push
|
||||
#pragma clang diagnostic ignored "-Wdeprecated-declarations"
|
||||
[[self openGLContext] setValues:&swapInt forParameter:NSOpenGLCPSwapInterval];
|
||||
#pragma clang diagnostic pop
|
||||
|
||||
// Using attribute arrays in OpenGL 3.3 requires the use of a VBA.
|
||||
// But VBAs don't exist in ES 2. So we bind a default one.
|
||||
GLuint vba;
|
||||
glGenVertexArrays(1, &vba);
|
||||
glBindVertexArray(vba);
|
||||
|
||||
startloop((GLintptr)[self openGLContext]);
|
||||
}
|
||||
|
||||
- (void)reshape {
|
||||
[super reshape];
|
||||
|
||||
// Calculate screen PPI.
|
||||
//
|
||||
// Note that the backingScaleFactor converts from logical
|
||||
// pixels to actual pixels, but both of these units vary
|
||||
// independently from real world size. E.g.
|
||||
//
|
||||
// 13" Retina Macbook Pro, 2560x1600, 227ppi, backingScaleFactor=2, scale=3.15
|
||||
// 15" Retina Macbook Pro, 2880x1800, 220ppi, backingScaleFactor=2, scale=3.06
|
||||
// 27" iMac, 2560x1440, 109ppi, backingScaleFactor=1, scale=1.51
|
||||
// 27" Retina iMac, 5120x2880, 218ppi, backingScaleFactor=2, scale=3.03
|
||||
NSScreen *screen = [NSScreen mainScreen];
|
||||
double screenPixW = [screen frame].size.width * [screen backingScaleFactor];
|
||||
|
||||
CGDirectDisplayID display = (CGDirectDisplayID)[[[screen deviceDescription] valueForKey:@"NSScreenNumber"] intValue];
|
||||
CGSize screenSizeMM = CGDisplayScreenSize(display); // in millimeters
|
||||
float ppi = 25.4 * screenPixW / screenSizeMM.width;
|
||||
float pixelsPerPt = ppi/72.0;
|
||||
|
||||
// The width and height reported to the geom package are the
|
||||
// bounds of the OpenGL view. Several steps are necessary.
|
||||
// First, [self bounds] gives us the number of logical pixels
|
||||
// in the view. Multiplying this by the backingScaleFactor
|
||||
// gives us the number of actual pixels.
|
||||
NSRect r = [self bounds];
|
||||
int w = r.size.width * [screen backingScaleFactor];
|
||||
int h = r.size.height * [screen backingScaleFactor];
|
||||
|
||||
setGeom(pixelsPerPt, w, h);
|
||||
}
|
||||
|
||||
- (void)drawRect:(NSRect)theRect {
|
||||
// Called during resize. This gets rid of flicker when resizing.
|
||||
drawgl();
|
||||
}
|
||||
|
||||
- (void)mouseDown:(NSEvent *)theEvent {
|
||||
double scale = [[NSScreen mainScreen] backingScaleFactor];
|
||||
NSPoint p = [theEvent locationInWindow];
|
||||
eventMouseDown(p.x * scale, p.y * scale);
|
||||
}
|
||||
|
||||
- (void)mouseUp:(NSEvent *)theEvent {
|
||||
double scale = [[NSScreen mainScreen] backingScaleFactor];
|
||||
NSPoint p = [theEvent locationInWindow];
|
||||
eventMouseEnd(p.x * scale, p.y * scale);
|
||||
}
|
||||
|
||||
- (void)mouseDragged:(NSEvent *)theEvent {
|
||||
double scale = [[NSScreen mainScreen] backingScaleFactor];
|
||||
NSPoint p = [theEvent locationInWindow];
|
||||
eventMouseDragged(p.x * scale, p.y * scale);
|
||||
}
|
||||
|
||||
- (void)windowDidBecomeKey:(NSNotification *)notification {
|
||||
lifecycleFocused();
|
||||
}
|
||||
|
||||
- (void)windowDidResignKey:(NSNotification *)notification {
|
||||
if (![NSApp isHidden]) {
|
||||
lifecycleVisible();
|
||||
}
|
||||
}
|
||||
|
||||
- (void)applicationDidFinishLaunching:(NSNotification *)aNotification {
|
||||
lifecycleAlive();
|
||||
[[NSRunningApplication currentApplication] activateWithOptions:(NSApplicationActivateAllWindows | NSApplicationActivateIgnoringOtherApps)];
|
||||
[self.window makeKeyAndOrderFront:self];
|
||||
lifecycleVisible();
|
||||
}
|
||||
|
||||
- (void)applicationWillTerminate:(NSNotification *)aNotification {
|
||||
lifecycleDead();
|
||||
}
|
||||
|
||||
- (void)applicationDidHide:(NSNotification *)aNotification {
|
||||
lifecycleAlive();
|
||||
}
|
||||
|
||||
- (void)applicationWillUnhide:(NSNotification *)notification {
|
||||
lifecycleVisible();
|
||||
}
|
||||
|
||||
- (void)windowWillClose:(NSNotification *)notification {
|
||||
lifecycleAlive();
|
||||
}
|
||||
@end
|
||||
|
||||
@interface MobileResponder : NSResponder
|
||||
{
|
||||
}
|
||||
@end
|
||||
|
||||
@implementation MobileResponder
|
||||
- (void)keyDown:(NSEvent *)theEvent {
|
||||
[self key:theEvent];
|
||||
}
|
||||
- (void)keyUp:(NSEvent *)theEvent {
|
||||
[self key:theEvent];
|
||||
}
|
||||
- (void)key:(NSEvent *)theEvent {
|
||||
NSRange range = [theEvent.characters rangeOfComposedCharacterSequenceAtIndex:0];
|
||||
|
||||
uint8_t buf[4] = {0, 0, 0, 0};
|
||||
if (![theEvent.characters getBytes:buf
|
||||
maxLength:4
|
||||
usedLength:nil
|
||||
encoding:NSUTF32LittleEndianStringEncoding
|
||||
options:NSStringEncodingConversionAllowLossy
|
||||
range:range
|
||||
remainingRange:nil]) {
|
||||
NSLog(@"failed to read key event %@", theEvent);
|
||||
return;
|
||||
}
|
||||
|
||||
uint32_t rune = (uint32_t)buf[0]<<0 | (uint32_t)buf[1]<<8 | (uint32_t)buf[2]<<16 | (uint32_t)buf[3]<<24;
|
||||
|
||||
uint8_t direction;
|
||||
if ([theEvent isARepeat]) {
|
||||
direction = 0;
|
||||
} else if (theEvent.type == NSEventTypeKeyDown) {
|
||||
direction = 1;
|
||||
} else {
|
||||
direction = 2;
|
||||
}
|
||||
eventKey((int32_t)rune, direction, theEvent.keyCode, theEvent.modifierFlags);
|
||||
}
|
||||
|
||||
- (void)flagsChanged:(NSEvent *)theEvent {
|
||||
eventFlags(theEvent.modifierFlags);
|
||||
}
|
||||
@end
|
||||
|
||||
void
|
||||
runApp(void) {
|
||||
[NSAutoreleasePool new];
|
||||
[NSApplication sharedApplication];
|
||||
[NSApp setActivationPolicy:NSApplicationActivationPolicyRegular];
|
||||
|
||||
id menuBar = [[NSMenu new] autorelease];
|
||||
id menuItem = [[NSMenuItem new] autorelease];
|
||||
[menuBar addItem:menuItem];
|
||||
[NSApp setMainMenu:menuBar];
|
||||
|
||||
id menu = [[NSMenu new] autorelease];
|
||||
id name = [[NSProcessInfo processInfo] processName];
|
||||
|
||||
id hideMenuItem = [[[NSMenuItem alloc] initWithTitle:@"Hide"
|
||||
action:@selector(hide:) keyEquivalent:@"h"]
|
||||
autorelease];
|
||||
[menu addItem:hideMenuItem];
|
||||
|
||||
id quitMenuItem = [[[NSMenuItem alloc] initWithTitle:@"Quit"
|
||||
action:@selector(terminate:) keyEquivalent:@"q"]
|
||||
autorelease];
|
||||
[menu addItem:quitMenuItem];
|
||||
[menuItem setSubmenu:menu];
|
||||
|
||||
NSRect rect = NSMakeRect(0, 0, 600, 800);
|
||||
|
||||
NSWindow* window = [[[NSWindow alloc] initWithContentRect:rect
|
||||
styleMask:NSWindowStyleMaskTitled
|
||||
backing:NSBackingStoreBuffered
|
||||
defer:NO]
|
||||
autorelease];
|
||||
window.styleMask |= NSWindowStyleMaskResizable;
|
||||
window.styleMask |= NSWindowStyleMaskMiniaturizable;
|
||||
window.styleMask |= NSWindowStyleMaskClosable;
|
||||
window.title = name;
|
||||
[window cascadeTopLeftFromPoint:NSMakePoint(20,20)];
|
||||
|
||||
NSOpenGLPixelFormatAttribute attr[] = {
|
||||
NSOpenGLPFAOpenGLProfile, NSOpenGLProfileVersion3_2Core,
|
||||
NSOpenGLPFAColorSize, 24,
|
||||
NSOpenGLPFAAlphaSize, 8,
|
||||
NSOpenGLPFADepthSize, 16,
|
||||
NSOpenGLPFAAccelerated,
|
||||
NSOpenGLPFADoubleBuffer,
|
||||
NSOpenGLPFAAllowOfflineRenderers,
|
||||
0
|
||||
};
|
||||
id pixFormat = [[NSOpenGLPixelFormat alloc] initWithAttributes:attr];
|
||||
MobileGLView* view = [[MobileGLView alloc] initWithFrame:rect pixelFormat:pixFormat];
|
||||
[window setContentView:view];
|
||||
[window setDelegate:view];
|
||||
[NSApp setDelegate:view];
|
||||
|
||||
window.nextResponder = [[[MobileResponder alloc] init] autorelease];
|
||||
|
||||
[NSApp run];
|
||||
}
|
||||
|
||||
void stopApp(void) {
|
||||
[NSApp terminate:nil];
|
||||
}
|
215
app/darwin_ios.go
Normal file
215
app/darwin_ios.go
Normal file
|
@ -0,0 +1,215 @@
|
|||
// Copyright 2015 The Go Authors. All rights reserved.
|
||||
// Use of this source code is governed by a BSD-style
|
||||
// license that can be found in the LICENSE file.
|
||||
|
||||
//go:build darwin && ios
|
||||
|
||||
package app
|
||||
|
||||
/*
|
||||
#cgo CFLAGS: -x objective-c -DGL_SILENCE_DEPRECATION -DGLES_SILENCE_DEPRECATION
|
||||
#cgo LDFLAGS: -framework Foundation -framework UIKit -framework GLKit -framework OpenGLES -framework QuartzCore
|
||||
#include <sys/utsname.h>
|
||||
#include <stdint.h>
|
||||
#include <pthread.h>
|
||||
#include <UIKit/UIDevice.h>
|
||||
#import <GLKit/GLKit.h>
|
||||
|
||||
extern struct utsname sysInfo;
|
||||
|
||||
void runApp(void);
|
||||
void makeCurrentContext(GLintptr ctx);
|
||||
void swapBuffers(GLintptr ctx);
|
||||
uint64_t threadID();
|
||||
*/
|
||||
import "C"
|
||||
import (
|
||||
"log"
|
||||
"runtime"
|
||||
"strings"
|
||||
"sync"
|
||||
|
||||
"golang.org/x/mobile/event/lifecycle"
|
||||
"golang.org/x/mobile/event/paint"
|
||||
"golang.org/x/mobile/event/size"
|
||||
"golang.org/x/mobile/event/touch"
|
||||
"golang.org/x/mobile/geom"
|
||||
)
|
||||
|
||||
var initThreadID uint64
|
||||
|
||||
func init() {
|
||||
// Lock the goroutine responsible for initialization to an OS thread.
|
||||
// This means the goroutine running main (and calling the run function
|
||||
// below) is locked to the OS thread that started the program. This is
|
||||
// necessary for the correct delivery of UIKit events to the process.
|
||||
//
|
||||
// A discussion on this topic:
|
||||
// https://groups.google.com/forum/#!msg/golang-nuts/IiWZ2hUuLDA/SNKYYZBelsYJ
|
||||
runtime.LockOSThread()
|
||||
initThreadID = uint64(C.threadID())
|
||||
}
|
||||
|
||||
func main(f func(App)) {
|
||||
if tid := uint64(C.threadID()); tid != initThreadID {
|
||||
log.Fatalf("app.Run called on thread %d, but app.init ran on %d", tid, initThreadID)
|
||||
}
|
||||
|
||||
go func() {
|
||||
f(theApp)
|
||||
// TODO(crawshaw): trigger runApp to return
|
||||
}()
|
||||
C.runApp()
|
||||
panic("unexpected return from app.runApp")
|
||||
}
|
||||
|
||||
var pixelsPerPt float32
|
||||
var screenScale int // [UIScreen mainScreen].scale, either 1, 2, or 3.
|
||||
|
||||
//export setScreen
|
||||
func setScreen(scale int) {
|
||||
C.uname(&C.sysInfo)
|
||||
name := C.GoString(&C.sysInfo.machine[0])
|
||||
|
||||
var v float32
|
||||
|
||||
switch {
|
||||
case strings.HasPrefix(name, "iPhone"):
|
||||
v = 163
|
||||
case strings.HasPrefix(name, "iPad"):
|
||||
// TODO: is there a better way to distinguish the iPad Mini?
|
||||
switch name {
|
||||
case "iPad2,5", "iPad2,6", "iPad2,7", "iPad4,4", "iPad4,5", "iPad4,6", "iPad4,7":
|
||||
v = 163 // iPad Mini
|
||||
default:
|
||||
v = 132
|
||||
}
|
||||
default:
|
||||
v = 163 // names like i386 and x86_64 are the simulator
|
||||
}
|
||||
|
||||
if v == 0 {
|
||||
log.Printf("unknown machine: %s", name)
|
||||
v = 163 // emergency fallback
|
||||
}
|
||||
|
||||
pixelsPerPt = v * float32(scale) / 72
|
||||
screenScale = scale
|
||||
}
|
||||
|
||||
//export updateConfig
|
||||
func updateConfig(width, height, orientation int32) {
|
||||
o := size.OrientationUnknown
|
||||
switch orientation {
|
||||
case C.UIDeviceOrientationPortrait, C.UIDeviceOrientationPortraitUpsideDown:
|
||||
o = size.OrientationPortrait
|
||||
case C.UIDeviceOrientationLandscapeLeft, C.UIDeviceOrientationLandscapeRight:
|
||||
o = size.OrientationLandscape
|
||||
}
|
||||
widthPx := screenScale * int(width)
|
||||
heightPx := screenScale * int(height)
|
||||
theApp.eventsIn <- size.Event{
|
||||
WidthPx: widthPx,
|
||||
HeightPx: heightPx,
|
||||
WidthPt: geom.Pt(float32(widthPx) / pixelsPerPt),
|
||||
HeightPt: geom.Pt(float32(heightPx) / pixelsPerPt),
|
||||
PixelsPerPt: pixelsPerPt,
|
||||
Orientation: o,
|
||||
}
|
||||
theApp.eventsIn <- paint.Event{External: true}
|
||||
}
|
||||
|
||||
// touchIDs is the current active touches. The position in the array
|
||||
// is the ID, the value is the UITouch* pointer value.
|
||||
//
|
||||
// It is widely reported that the iPhone can handle up to 5 simultaneous
|
||||
// touch events, while the iPad can handle 11.
|
||||
var touchIDs [11]uintptr
|
||||
|
||||
var touchEvents struct {
|
||||
sync.Mutex
|
||||
pending []touch.Event
|
||||
}
|
||||
|
||||
//export sendTouch
|
||||
func sendTouch(cTouch, cTouchType uintptr, x, y float32) {
|
||||
id := -1
|
||||
for i, val := range touchIDs {
|
||||
if val == cTouch {
|
||||
id = i
|
||||
break
|
||||
}
|
||||
}
|
||||
if id == -1 {
|
||||
for i, val := range touchIDs {
|
||||
if val == 0 {
|
||||
touchIDs[i] = cTouch
|
||||
id = i
|
||||
break
|
||||
}
|
||||
}
|
||||
if id == -1 {
|
||||
panic("out of touchIDs")
|
||||
}
|
||||
}
|
||||
|
||||
t := touch.Type(cTouchType)
|
||||
if t == touch.TypeEnd {
|
||||
touchIDs[id] = 0
|
||||
}
|
||||
|
||||
theApp.eventsIn <- touch.Event{
|
||||
X: x,
|
||||
Y: y,
|
||||
Sequence: touch.Sequence(id),
|
||||
Type: t,
|
||||
}
|
||||
}
|
||||
|
||||
//export lifecycleDead
|
||||
func lifecycleDead() { theApp.sendLifecycle(lifecycle.StageDead) }
|
||||
|
||||
//export lifecycleAlive
|
||||
func lifecycleAlive() { theApp.sendLifecycle(lifecycle.StageAlive) }
|
||||
|
||||
//export lifecycleVisible
|
||||
func lifecycleVisible() { theApp.sendLifecycle(lifecycle.StageVisible) }
|
||||
|
||||
//export lifecycleFocused
|
||||
func lifecycleFocused() { theApp.sendLifecycle(lifecycle.StageFocused) }
|
||||
|
||||
//export startloop
|
||||
func startloop(ctx C.GLintptr) {
|
||||
go theApp.loop(ctx)
|
||||
}
|
||||
|
||||
// loop is the primary drawing loop.
|
||||
//
|
||||
// After UIKit has captured the initial OS thread for processing UIKit
|
||||
// events in runApp, it starts loop on another goroutine. It is locked
|
||||
// to an OS thread for its OpenGL context.
|
||||
func (a *app) loop(ctx C.GLintptr) {
|
||||
runtime.LockOSThread()
|
||||
C.makeCurrentContext(ctx)
|
||||
|
||||
workAvailable := a.worker.WorkAvailable()
|
||||
|
||||
for {
|
||||
select {
|
||||
case <-workAvailable:
|
||||
a.worker.DoWork()
|
||||
case <-theApp.publish:
|
||||
loop1:
|
||||
for {
|
||||
select {
|
||||
case <-workAvailable:
|
||||
a.worker.DoWork()
|
||||
default:
|
||||
break loop1
|
||||
}
|
||||
}
|
||||
C.swapBuffers(ctx)
|
||||
theApp.publishResult <- PublishResult{}
|
||||
}
|
||||
}
|
||||
}
|
167
app/darwin_ios.m
Normal file
167
app/darwin_ios.m
Normal file
|
@ -0,0 +1,167 @@
|
|||
// Copyright 2015 The Go Authors. All rights reserved.
|
||||
// Use of this source code is governed by a BSD-style
|
||||
// license that can be found in the LICENSE file.
|
||||
|
||||
//go:build darwin && ios
|
||||
// +build darwin
|
||||
// +build ios
|
||||
|
||||
#include "_cgo_export.h"
|
||||
#include <pthread.h>
|
||||
#include <stdio.h>
|
||||
#include <sys/utsname.h>
|
||||
|
||||
#import <UIKit/UIKit.h>
|
||||
#import <GLKit/GLKit.h>
|
||||
|
||||
struct utsname sysInfo;
|
||||
|
||||
@interface GoAppAppController : GLKViewController<UIContentContainer, GLKViewDelegate>
|
||||
@end
|
||||
|
||||
@interface GoAppAppDelegate : UIResponder<UIApplicationDelegate>
|
||||
@property (strong, nonatomic) UIWindow *window;
|
||||
@property (strong, nonatomic) GoAppAppController *controller;
|
||||
@end
|
||||
|
||||
@implementation GoAppAppDelegate
|
||||
- (BOOL)application:(UIApplication *)application didFinishLaunchingWithOptions:(NSDictionary *)launchOptions {
|
||||
lifecycleAlive();
|
||||
self.window = [[UIWindow alloc] initWithFrame:[[UIScreen mainScreen] bounds]];
|
||||
self.controller = [[GoAppAppController alloc] initWithNibName:nil bundle:nil];
|
||||
self.window.rootViewController = self.controller;
|
||||
[self.window makeKeyAndVisible];
|
||||
return YES;
|
||||
}
|
||||
|
||||
- (void)applicationDidBecomeActive:(UIApplication * )application {
|
||||
lifecycleFocused();
|
||||
}
|
||||
|
||||
- (void)applicationWillResignActive:(UIApplication *)application {
|
||||
lifecycleVisible();
|
||||
}
|
||||
|
||||
- (void)applicationDidEnterBackground:(UIApplication *)application {
|
||||
lifecycleAlive();
|
||||
}
|
||||
|
||||
- (void)applicationWillTerminate:(UIApplication *)application {
|
||||
lifecycleDead();
|
||||
}
|
||||
@end
|
||||
|
||||
@interface GoAppAppController ()
|
||||
@property (strong, nonatomic) EAGLContext *context;
|
||||
@property (strong, nonatomic) GLKView *glview;
|
||||
@end
|
||||
|
||||
@implementation GoAppAppController
|
||||
- (void)viewWillAppear:(BOOL)animated
|
||||
{
|
||||
// TODO: replace by swapping out GLKViewController for a UIVIewController.
|
||||
[super viewWillAppear:animated];
|
||||
self.paused = YES;
|
||||
}
|
||||
|
||||
- (void)viewDidLoad {
|
||||
[super viewDidLoad];
|
||||
self.context = [[EAGLContext alloc] initWithAPI:kEAGLRenderingAPIOpenGLES2];
|
||||
self.glview = (GLKView*)self.view;
|
||||
self.glview.drawableDepthFormat = GLKViewDrawableDepthFormat24;
|
||||
self.glview.multipleTouchEnabled = true; // TODO expose setting to user.
|
||||
self.glview.context = self.context;
|
||||
self.glview.userInteractionEnabled = YES;
|
||||
self.glview.enableSetNeedsDisplay = YES; // only invoked once
|
||||
|
||||
// Do not use the GLKViewController draw loop.
|
||||
self.paused = YES;
|
||||
self.resumeOnDidBecomeActive = NO;
|
||||
self.preferredFramesPerSecond = 0;
|
||||
|
||||
int scale = 1;
|
||||
if ([[UIScreen mainScreen] respondsToSelector:@selector(displayLinkWithTarget:selector:)]) {
|
||||
scale = (int)[UIScreen mainScreen].scale; // either 1.0, 2.0, or 3.0.
|
||||
}
|
||||
setScreen(scale);
|
||||
|
||||
CGSize size = [UIScreen mainScreen].bounds.size;
|
||||
UIInterfaceOrientation orientation = [[UIApplication sharedApplication] statusBarOrientation];
|
||||
updateConfig((int)size.width, (int)size.height, orientation);
|
||||
}
|
||||
|
||||
- (void)viewWillTransitionToSize:(CGSize)size withTransitionCoordinator:(id<UIViewControllerTransitionCoordinator>)coordinator {
|
||||
[coordinator animateAlongsideTransition:^(id<UIViewControllerTransitionCoordinatorContext> context) {
|
||||
// TODO(crawshaw): come up with a plan to handle animations.
|
||||
} completion:^(id<UIViewControllerTransitionCoordinatorContext> context) {
|
||||
UIInterfaceOrientation orientation = [[UIApplication sharedApplication] statusBarOrientation];
|
||||
updateConfig((int)size.width, (int)size.height, orientation);
|
||||
}];
|
||||
}
|
||||
|
||||
- (void)glkView:(GLKView *)view drawInRect:(CGRect)rect {
|
||||
// Now that we have been asked to do the first draw, disable any
|
||||
// future draw and hand control over to the Go paint.Event cycle.
|
||||
self.glview.enableSetNeedsDisplay = NO;
|
||||
startloop((GLintptr)self.context);
|
||||
}
|
||||
|
||||
#define TOUCH_TYPE_BEGIN 0 // touch.TypeBegin
|
||||
#define TOUCH_TYPE_MOVE 1 // touch.TypeMove
|
||||
#define TOUCH_TYPE_END 2 // touch.TypeEnd
|
||||
|
||||
static void sendTouches(int change, NSSet* touches) {
|
||||
CGFloat scale = [UIScreen mainScreen].scale;
|
||||
for (UITouch* touch in touches) {
|
||||
CGPoint p = [touch locationInView:touch.view];
|
||||
sendTouch((GoUintptr)touch, (GoUintptr)change, p.x*scale, p.y*scale);
|
||||
}
|
||||
}
|
||||
|
||||
- (void)touchesBegan:(NSSet*)touches withEvent:(UIEvent*)event {
|
||||
sendTouches(TOUCH_TYPE_BEGIN, touches);
|
||||
}
|
||||
|
||||
- (void)touchesMoved:(NSSet*)touches withEvent:(UIEvent*)event {
|
||||
sendTouches(TOUCH_TYPE_MOVE, touches);
|
||||
}
|
||||
|
||||
- (void)touchesEnded:(NSSet*)touches withEvent:(UIEvent*)event {
|
||||
sendTouches(TOUCH_TYPE_END, touches);
|
||||
}
|
||||
|
||||
- (void)touchesCanceled:(NSSet*)touches withEvent:(UIEvent*)event {
|
||||
sendTouches(TOUCH_TYPE_END, touches);
|
||||
}
|
||||
@end
|
||||
|
||||
void runApp(void) {
|
||||
char* argv[] = {};
|
||||
@autoreleasepool {
|
||||
UIApplicationMain(0, argv, nil, NSStringFromClass([GoAppAppDelegate class]));
|
||||
}
|
||||
}
|
||||
|
||||
void makeCurrentContext(GLintptr context) {
|
||||
EAGLContext* ctx = (EAGLContext*)context;
|
||||
if (![EAGLContext setCurrentContext:ctx]) {
|
||||
// TODO(crawshaw): determine how terrible this is. Exit?
|
||||
NSLog(@"failed to set current context");
|
||||
}
|
||||
}
|
||||
|
||||
void swapBuffers(GLintptr context) {
|
||||
__block EAGLContext* ctx = (EAGLContext*)context;
|
||||
dispatch_sync(dispatch_get_main_queue(), ^{
|
||||
[EAGLContext setCurrentContext:ctx];
|
||||
[ctx presentRenderbuffer:GL_RENDERBUFFER];
|
||||
});
|
||||
}
|
||||
|
||||
uint64_t threadID() {
|
||||
uint64_t id;
|
||||
if (pthread_threadid_np(pthread_self(), &id)) {
|
||||
abort();
|
||||
}
|
||||
return id;
|
||||
}
|
88
app/doc.go
Normal file
88
app/doc.go
Normal file
|
@ -0,0 +1,88 @@
|
|||
// Copyright 2014 The Go Authors. All rights reserved.
|
||||
// Use of this source code is governed by a BSD-style
|
||||
// license that can be found in the LICENSE file.
|
||||
|
||||
/*
|
||||
Package app lets you write portable all-Go apps for Android and iOS.
|
||||
|
||||
There are typically two ways to use Go on Android and iOS. The first
|
||||
is to write a Go library and use `gomobile bind` to generate language
|
||||
bindings for Java and Objective-C. Building a library does not
|
||||
require the app package. The `gomobile bind` command produces output
|
||||
that you can include in an Android Studio or Xcode project. For more
|
||||
on language bindings, see https://golang.org/x/mobile/cmd/gobind.
|
||||
|
||||
The second way is to write an app entirely in Go. The APIs are limited
|
||||
to those that are portable between both Android and iOS, in particular
|
||||
OpenGL, audio, and other Android NDK-like APIs. An all-Go app should
|
||||
use this app package to initialize the app, manage its lifecycle, and
|
||||
receive events.
|
||||
|
||||
# Building apps
|
||||
|
||||
Apps written entirely in Go have a main function, and can be built
|
||||
with `gomobile build`, which directly produces runnable output for
|
||||
Android and iOS.
|
||||
|
||||
The gomobile tool can get installed with go get. For reference, see
|
||||
https://golang.org/x/mobile/cmd/gomobile.
|
||||
|
||||
For detailed instructions and documentation, see
|
||||
https://golang.org/wiki/Mobile.
|
||||
|
||||
# Event processing in Native Apps
|
||||
|
||||
The Go runtime is initialized on Android when NativeActivity onCreate is
|
||||
called, and on iOS when the process starts. In both cases, Go init functions
|
||||
run before the app lifecycle has started.
|
||||
|
||||
An app is expected to call the Main function in main.main. When the function
|
||||
exits, the app exits. Inside the func passed to Main, call Filter on every
|
||||
event received, and then switch on its type. Registered filters run when the
|
||||
event is received, not when it is sent, so that filters run in the same
|
||||
goroutine as other code that calls OpenGL.
|
||||
|
||||
package main
|
||||
|
||||
import (
|
||||
"log"
|
||||
|
||||
"golang.org/x/mobile/app"
|
||||
"golang.org/x/mobile/event/lifecycle"
|
||||
"golang.org/x/mobile/event/paint"
|
||||
)
|
||||
|
||||
func main() {
|
||||
app.Main(func(a app.App) {
|
||||
for e := range a.Events() {
|
||||
switch e := a.Filter(e).(type) {
|
||||
case lifecycle.Event:
|
||||
// ...
|
||||
case paint.Event:
|
||||
log.Print("Call OpenGL here.")
|
||||
a.Publish()
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
An event is represented by the empty interface type interface{}. Any value can
|
||||
be an event. Commonly used types include Event types defined by the following
|
||||
packages:
|
||||
- golang.org/x/mobile/event/lifecycle
|
||||
- golang.org/x/mobile/event/mouse
|
||||
- golang.org/x/mobile/event/paint
|
||||
- golang.org/x/mobile/event/size
|
||||
- golang.org/x/mobile/event/touch
|
||||
|
||||
For example, touch.Event is the type that represents touch events. Other
|
||||
packages may define their own events, and send them on an app's event channel.
|
||||
|
||||
Other packages can also register event filters, e.g. to manage resources in
|
||||
response to lifecycle events. Such packages should call:
|
||||
|
||||
app.RegisterFilter(etc)
|
||||
|
||||
in an init function inside that package.
|
||||
*/
|
||||
package app
|
67
app/internal/apptest/apptest.go
Normal file
67
app/internal/apptest/apptest.go
Normal file
|
@ -0,0 +1,67 @@
|
|||
// Copyright 2015 The Go Authors. All rights reserved.
|
||||
// Use of this source code is governed by a BSD-style
|
||||
// license that can be found in the LICENSE file.
|
||||
|
||||
// Package apptest provides utilities for testing an app.
|
||||
//
|
||||
// It is extremely incomplete, hence it being internal.
|
||||
// For starters, it should support iOS.
|
||||
package apptest
|
||||
|
||||
import (
|
||||
"bufio"
|
||||
"bytes"
|
||||
"fmt"
|
||||
"net"
|
||||
)
|
||||
|
||||
// Port is the TCP port used to communicate with the test app.
|
||||
//
|
||||
// TODO(crawshaw): find a way to make this configurable. adb am extras?
|
||||
const Port = "12533"
|
||||
|
||||
// Comm is a simple text-based communication protocol.
|
||||
//
|
||||
// Assumes all sides are friendly and cooperative and that the
|
||||
// communication is over at the first sign of trouble.
|
||||
type Comm struct {
|
||||
Conn net.Conn
|
||||
Fatalf func(format string, args ...interface{})
|
||||
Printf func(format string, args ...interface{})
|
||||
|
||||
scanner *bufio.Scanner
|
||||
}
|
||||
|
||||
func (c *Comm) Send(cmd string, args ...interface{}) {
|
||||
buf := new(bytes.Buffer)
|
||||
buf.WriteString(cmd)
|
||||
for _, arg := range args {
|
||||
buf.WriteRune(' ')
|
||||
fmt.Fprintf(buf, "%v", arg)
|
||||
}
|
||||
buf.WriteRune('\n')
|
||||
b := buf.Bytes()
|
||||
c.Printf("comm.send: %s\n", b)
|
||||
if _, err := c.Conn.Write(b); err != nil {
|
||||
c.Fatalf("failed to send %s: %v", b, err)
|
||||
}
|
||||
}
|
||||
|
||||
func (c *Comm) Recv(cmd string, a ...interface{}) {
|
||||
if c.scanner == nil {
|
||||
c.scanner = bufio.NewScanner(c.Conn)
|
||||
}
|
||||
if !c.scanner.Scan() {
|
||||
c.Fatalf("failed to recv %q: %v", cmd, c.scanner.Err())
|
||||
}
|
||||
text := c.scanner.Text()
|
||||
c.Printf("comm.recv: %s\n", text)
|
||||
var recvCmd string
|
||||
args := append([]interface{}{&recvCmd}, a...)
|
||||
if _, err := fmt.Sscan(text, args...); err != nil {
|
||||
c.Fatalf("cannot scan recv command %s: %q: %v", cmd, text, err)
|
||||
}
|
||||
if cmd != recvCmd {
|
||||
c.Fatalf("expecting recv %q, got %v", cmd, text)
|
||||
}
|
||||
}
|
16
app/internal/callfn/callfn.go
Normal file
16
app/internal/callfn/callfn.go
Normal file
|
@ -0,0 +1,16 @@
|
|||
// Copyright 2015 The Go Authors. All rights reserved.
|
||||
// Use of this source code is governed by a BSD-style
|
||||
// license that can be found in the LICENSE file.
|
||||
|
||||
//go:build android && (arm || 386 || amd64 || arm64)
|
||||
|
||||
// Package callfn provides an android entry point.
|
||||
//
|
||||
// It is a separate package from app because it contains Go assembly,
|
||||
// which does not compile in a package using cgo.
|
||||
package callfn
|
||||
|
||||
// CallFn calls a zero-argument function by its program counter.
|
||||
// It is only intended for calling main.main. Using it for
|
||||
// anything else will not end well.
|
||||
func CallFn(fn uintptr)
|
11
app/internal/callfn/callfn_386.s
Normal file
11
app/internal/callfn/callfn_386.s
Normal file
|
@ -0,0 +1,11 @@
|
|||
// Copyright 2015 The Go Authors. All rights reserved.
|
||||
// Use of this source code is governed by a BSD-style
|
||||
// license that can be found in the LICENSE file.
|
||||
|
||||
#include "textflag.h"
|
||||
#include "funcdata.h"
|
||||
|
||||
TEXT ·CallFn(SB),$0-4
|
||||
MOVL fn+0(FP), AX
|
||||
CALL AX
|
||||
RET
|
11
app/internal/callfn/callfn_amd64.s
Normal file
11
app/internal/callfn/callfn_amd64.s
Normal file
|
@ -0,0 +1,11 @@
|
|||
// Copyright 2015 The Go Authors. All rights reserved.
|
||||
// Use of this source code is governed by a BSD-style
|
||||
// license that can be found in the LICENSE file.
|
||||
|
||||
#include "textflag.h"
|
||||
#include "funcdata.h"
|
||||
|
||||
TEXT ·CallFn(SB),$0-8
|
||||
MOVQ fn+0(FP), AX
|
||||
CALL AX
|
||||
RET
|
11
app/internal/callfn/callfn_arm.s
Normal file
11
app/internal/callfn/callfn_arm.s
Normal file
|
@ -0,0 +1,11 @@
|
|||
// Copyright 2015 The Go Authors. All rights reserved.
|
||||
// Use of this source code is governed by a BSD-style
|
||||
// license that can be found in the LICENSE file.
|
||||
|
||||
#include "textflag.h"
|
||||
#include "funcdata.h"
|
||||
|
||||
TEXT ·CallFn(SB),$0-4
|
||||
MOVW fn+0(FP), R0
|
||||
BL (R0)
|
||||
RET
|
11
app/internal/callfn/callfn_arm64.s
Normal file
11
app/internal/callfn/callfn_arm64.s
Normal file
|
@ -0,0 +1,11 @@
|
|||
// Copyright 2016 The Go Authors. All rights reserved.
|
||||
// Use of this source code is governed by a BSD-style
|
||||
// license that can be found in the LICENSE file.
|
||||
|
||||
#include "textflag.h"
|
||||
#include "funcdata.h"
|
||||
|
||||
TEXT ·CallFn(SB),$0-8
|
||||
MOVD fn+0(FP), R0
|
||||
BL (R0)
|
||||
RET
|
27
app/internal/testapp/AndroidManifest.xml
Normal file
27
app/internal/testapp/AndroidManifest.xml
Normal file
|
@ -0,0 +1,27 @@
|
|||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<!--
|
||||
Copyright 2015 The Go Authors. All rights reserved.
|
||||
Use of this source code is governed by a BSD-style
|
||||
license that can be found in the LICENSE file.
|
||||
-->
|
||||
<manifest
|
||||
xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
package="org.golang.testapp"
|
||||
android:versionCode="1"
|
||||
android:versionName="1.0">
|
||||
|
||||
<uses-sdk android:minSdkVersion="15" />
|
||||
<!-- to talk to the host -->
|
||||
<uses-permission android:name="android.permission.INTERNET" />
|
||||
<application android:label="testapp" android:debuggable="true">
|
||||
<activity android:name="org.golang.app.GoNativeActivity"
|
||||
android:label="testapp"
|
||||
android:configChanges="orientation|keyboardHidden">
|
||||
<meta-data android:name="android.app.lib_name" android:value="testapp" />
|
||||
<intent-filter>
|
||||
<action android:name="android.intent.action.MAIN" />
|
||||
<category android:name="android.intent.category.LAUNCHER" />
|
||||
</intent-filter>
|
||||
</activity>
|
||||
</application>
|
||||
</manifest>
|
94
app/internal/testapp/testapp.go
Normal file
94
app/internal/testapp/testapp.go
Normal file
|
@ -0,0 +1,94 @@
|
|||
// Copyright 2015 The Go Authors. All rights reserved.
|
||||
// Use of this source code is governed by a BSD-style
|
||||
// license that can be found in the LICENSE file.
|
||||
|
||||
//go:build darwin || linux
|
||||
|
||||
// Small test app used by app/app_test.go.
|
||||
package main
|
||||
|
||||
import (
|
||||
"log"
|
||||
"net"
|
||||
|
||||
"golang.org/x/mobile/app"
|
||||
"golang.org/x/mobile/app/internal/apptest"
|
||||
"golang.org/x/mobile/event/lifecycle"
|
||||
"golang.org/x/mobile/event/paint"
|
||||
"golang.org/x/mobile/event/size"
|
||||
"golang.org/x/mobile/event/touch"
|
||||
"golang.org/x/mobile/gl"
|
||||
)
|
||||
|
||||
func main() {
|
||||
app.Main(func(a app.App) {
|
||||
var (
|
||||
glctx gl.Context
|
||||
visible bool
|
||||
)
|
||||
|
||||
addr := "127.0.0.1:" + apptest.Port
|
||||
log.Printf("addr: %s", addr)
|
||||
|
||||
conn, err := net.Dial("tcp", addr)
|
||||
if err != nil {
|
||||
log.Fatal(err)
|
||||
}
|
||||
defer conn.Close()
|
||||
log.Printf("dialled")
|
||||
comm := &apptest.Comm{
|
||||
Conn: conn,
|
||||
Fatalf: log.Panicf,
|
||||
Printf: log.Printf,
|
||||
}
|
||||
|
||||
comm.Send("hello_from_testapp")
|
||||
comm.Recv("hello_from_host")
|
||||
|
||||
color := "red"
|
||||
sendPainting := false
|
||||
for e := range a.Events() {
|
||||
switch e := a.Filter(e).(type) {
|
||||
case lifecycle.Event:
|
||||
switch e.Crosses(lifecycle.StageVisible) {
|
||||
case lifecycle.CrossOn:
|
||||
comm.Send("lifecycle_visible")
|
||||
sendPainting = true
|
||||
visible = true
|
||||
glctx, _ = e.DrawContext.(gl.Context)
|
||||
case lifecycle.CrossOff:
|
||||
comm.Send("lifecycle_not_visible")
|
||||
visible = false
|
||||
}
|
||||
case size.Event:
|
||||
comm.Send("size", e.PixelsPerPt, e.Orientation)
|
||||
case paint.Event:
|
||||
if visible {
|
||||
if color == "red" {
|
||||
glctx.ClearColor(1, 0, 0, 1)
|
||||
} else {
|
||||
glctx.ClearColor(0, 1, 0, 1)
|
||||
}
|
||||
glctx.Clear(gl.COLOR_BUFFER_BIT)
|
||||
a.Publish()
|
||||
}
|
||||
if sendPainting {
|
||||
comm.Send("paint", color)
|
||||
sendPainting = false
|
||||
}
|
||||
case touch.Event:
|
||||
comm.Send("touch", e.Type, e.X, e.Y)
|
||||
if e.Type == touch.TypeEnd {
|
||||
if color == "red" {
|
||||
color = "green"
|
||||
} else {
|
||||
color = "red"
|
||||
}
|
||||
sendPainting = true
|
||||
// Send a paint event so the screen gets redrawn.
|
||||
a.Send(paint.Event{})
|
||||
}
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
83
app/shiny.go
Normal file
83
app/shiny.go
Normal file
|
@ -0,0 +1,83 @@
|
|||
// Copyright 2015 The Go Authors. All rights reserved.
|
||||
// Use of this source code is governed by a BSD-style
|
||||
// license that can be found in the LICENSE file.
|
||||
|
||||
//go:build windows
|
||||
|
||||
package app
|
||||
|
||||
import (
|
||||
"log"
|
||||
|
||||
"golang.org/x/exp/shiny/driver/gldriver"
|
||||
"golang.org/x/exp/shiny/screen"
|
||||
"golang.org/x/mobile/event/lifecycle"
|
||||
"golang.org/x/mobile/event/mouse"
|
||||
"golang.org/x/mobile/event/touch"
|
||||
"golang.org/x/mobile/gl"
|
||||
)
|
||||
|
||||
func main(f func(a App)) {
|
||||
gldriver.Main(func(s screen.Screen) {
|
||||
w, err := s.NewWindow(nil)
|
||||
if err != nil {
|
||||
log.Fatal(err)
|
||||
}
|
||||
defer w.Release()
|
||||
|
||||
theApp.glctx = nil
|
||||
theApp.worker = nil // handled by shiny
|
||||
|
||||
go func() {
|
||||
for range theApp.publish {
|
||||
res := w.Publish()
|
||||
theApp.publishResult <- PublishResult{
|
||||
BackBufferPreserved: res.BackBufferPreserved,
|
||||
}
|
||||
}
|
||||
}()
|
||||
|
||||
donec := make(chan struct{})
|
||||
go func() {
|
||||
// close the donec channel in a defer statement
|
||||
// so that we could still be able to return even
|
||||
// if f panics.
|
||||
defer close(donec)
|
||||
|
||||
f(theApp)
|
||||
}()
|
||||
|
||||
for {
|
||||
select {
|
||||
case <-donec:
|
||||
return
|
||||
default:
|
||||
theApp.Send(convertEvent(w.NextEvent()))
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
func convertEvent(e interface{}) interface{} {
|
||||
switch e := e.(type) {
|
||||
case lifecycle.Event:
|
||||
if theApp.glctx == nil {
|
||||
theApp.glctx = e.DrawContext.(gl.Context)
|
||||
}
|
||||
case mouse.Event:
|
||||
te := touch.Event{
|
||||
X: e.X,
|
||||
Y: e.Y,
|
||||
}
|
||||
switch e.Direction {
|
||||
case mouse.DirNone:
|
||||
te.Type = touch.TypeMove
|
||||
case mouse.DirPress:
|
||||
te.Type = touch.TypeBegin
|
||||
case mouse.DirRelease:
|
||||
te.Type = touch.TypeEnd
|
||||
}
|
||||
return te
|
||||
}
|
||||
return e
|
||||
}
|
175
app/x11.c
Normal file
175
app/x11.c
Normal file
|
@ -0,0 +1,175 @@
|
|||
// Copyright 2014 The Go Authors. All rights reserved.
|
||||
// Use of this source code is governed by a BSD-style
|
||||
// license that can be found in the LICENSE file.
|
||||
|
||||
//go:build linux && !android
|
||||
// +build linux,!android
|
||||
|
||||
#include "_cgo_export.h"
|
||||
#include <EGL/egl.h>
|
||||
#include <GLES2/gl2.h>
|
||||
#include <X11/Xlib.h>
|
||||
#include <X11/Xutil.h>
|
||||
#include <stdio.h>
|
||||
#include <stdlib.h>
|
||||
|
||||
static Atom wm_delete_window;
|
||||
|
||||
static Window
|
||||
new_window(Display *x_dpy, EGLDisplay e_dpy, int w, int h, EGLContext *ctx, EGLSurface *surf) {
|
||||
static const EGLint attribs[] = {
|
||||
EGL_RENDERABLE_TYPE, EGL_OPENGL_ES2_BIT,
|
||||
EGL_SURFACE_TYPE, EGL_WINDOW_BIT,
|
||||
EGL_BLUE_SIZE, 8,
|
||||
EGL_GREEN_SIZE, 8,
|
||||
EGL_RED_SIZE, 8,
|
||||
EGL_DEPTH_SIZE, 16,
|
||||
EGL_CONFIG_CAVEAT, EGL_NONE,
|
||||
EGL_NONE
|
||||
};
|
||||
EGLConfig config;
|
||||
EGLint num_configs;
|
||||
if (!eglChooseConfig(e_dpy, attribs, &config, 1, &num_configs)) {
|
||||
fprintf(stderr, "eglChooseConfig failed\n");
|
||||
exit(1);
|
||||
}
|
||||
EGLint vid;
|
||||
if (!eglGetConfigAttrib(e_dpy, config, EGL_NATIVE_VISUAL_ID, &vid)) {
|
||||
fprintf(stderr, "eglGetConfigAttrib failed\n");
|
||||
exit(1);
|
||||
}
|
||||
|
||||
XVisualInfo visTemplate;
|
||||
visTemplate.visualid = vid;
|
||||
int num_visuals;
|
||||
XVisualInfo *visInfo = XGetVisualInfo(x_dpy, VisualIDMask, &visTemplate, &num_visuals);
|
||||
if (!visInfo) {
|
||||
fprintf(stderr, "XGetVisualInfo failed\n");
|
||||
exit(1);
|
||||
}
|
||||
|
||||
Window root = RootWindow(x_dpy, DefaultScreen(x_dpy));
|
||||
XSetWindowAttributes attr;
|
||||
|
||||
attr.colormap = XCreateColormap(x_dpy, root, visInfo->visual, AllocNone);
|
||||
if (!attr.colormap) {
|
||||
fprintf(stderr, "XCreateColormap failed\n");
|
||||
exit(1);
|
||||
}
|
||||
|
||||
attr.event_mask = StructureNotifyMask | ExposureMask |
|
||||
ButtonPressMask | ButtonReleaseMask | ButtonMotionMask;
|
||||
Window win = XCreateWindow(
|
||||
x_dpy, root, 0, 0, w, h, 0, visInfo->depth, InputOutput,
|
||||
visInfo->visual, CWColormap | CWEventMask, &attr);
|
||||
XFree(visInfo);
|
||||
|
||||
XSizeHints sizehints;
|
||||
sizehints.width = w;
|
||||
sizehints.height = h;
|
||||
sizehints.flags = USSize;
|
||||
XSetNormalHints(x_dpy, win, &sizehints);
|
||||
XSetStandardProperties(x_dpy, win, "App", "App", None, (char **)NULL, 0, &sizehints);
|
||||
|
||||
static const EGLint ctx_attribs[] = {
|
||||
EGL_CONTEXT_CLIENT_VERSION, 2,
|
||||
EGL_NONE
|
||||
};
|
||||
*ctx = eglCreateContext(e_dpy, config, EGL_NO_CONTEXT, ctx_attribs);
|
||||
if (!*ctx) {
|
||||
fprintf(stderr, "eglCreateContext failed\n");
|
||||
exit(1);
|
||||
}
|
||||
*surf = eglCreateWindowSurface(e_dpy, config, win, NULL);
|
||||
if (!*surf) {
|
||||
fprintf(stderr, "eglCreateWindowSurface failed\n");
|
||||
exit(1);
|
||||
}
|
||||
return win;
|
||||
}
|
||||
|
||||
Display *x_dpy;
|
||||
EGLDisplay e_dpy;
|
||||
EGLContext e_ctx;
|
||||
EGLSurface e_surf;
|
||||
Window win;
|
||||
|
||||
void
|
||||
createWindow(void) {
|
||||
x_dpy = XOpenDisplay(NULL);
|
||||
if (!x_dpy) {
|
||||
fprintf(stderr, "XOpenDisplay failed\n");
|
||||
exit(1);
|
||||
}
|
||||
e_dpy = eglGetDisplay(x_dpy);
|
||||
if (!e_dpy) {
|
||||
fprintf(stderr, "eglGetDisplay failed\n");
|
||||
exit(1);
|
||||
}
|
||||
EGLint e_major, e_minor;
|
||||
if (!eglInitialize(e_dpy, &e_major, &e_minor)) {
|
||||
fprintf(stderr, "eglInitialize failed\n");
|
||||
exit(1);
|
||||
}
|
||||
eglBindAPI(EGL_OPENGL_ES_API);
|
||||
win = new_window(x_dpy, e_dpy, 600, 800, &e_ctx, &e_surf);
|
||||
|
||||
wm_delete_window = XInternAtom(x_dpy, "WM_DELETE_WINDOW", True);
|
||||
if (wm_delete_window != None) {
|
||||
XSetWMProtocols(x_dpy, win, &wm_delete_window, 1);
|
||||
}
|
||||
|
||||
XMapWindow(x_dpy, win);
|
||||
if (!eglMakeCurrent(e_dpy, e_surf, e_surf, e_ctx)) {
|
||||
fprintf(stderr, "eglMakeCurrent failed\n");
|
||||
exit(1);
|
||||
}
|
||||
|
||||
// Window size and DPI should be initialized before starting app.
|
||||
XEvent ev;
|
||||
while (1) {
|
||||
if (XCheckMaskEvent(x_dpy, StructureNotifyMask, &ev) == False) {
|
||||
continue;
|
||||
}
|
||||
if (ev.type == ConfigureNotify) {
|
||||
onResize(ev.xconfigure.width, ev.xconfigure.height);
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
void
|
||||
processEvents(void) {
|
||||
while (XPending(x_dpy)) {
|
||||
XEvent ev;
|
||||
XNextEvent(x_dpy, &ev);
|
||||
switch (ev.type) {
|
||||
case ButtonPress:
|
||||
onTouchBegin((float)ev.xbutton.x, (float)ev.xbutton.y);
|
||||
break;
|
||||
case ButtonRelease:
|
||||
onTouchEnd((float)ev.xbutton.x, (float)ev.xbutton.y);
|
||||
break;
|
||||
case MotionNotify:
|
||||
onTouchMove((float)ev.xmotion.x, (float)ev.xmotion.y);
|
||||
break;
|
||||
case ConfigureNotify:
|
||||
onResize(ev.xconfigure.width, ev.xconfigure.height);
|
||||
break;
|
||||
case ClientMessage:
|
||||
if (wm_delete_window != None && (Atom)ev.xclient.data.l[0] == wm_delete_window) {
|
||||
onStop();
|
||||
return;
|
||||
}
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
void
|
||||
swapBuffers(void) {
|
||||
if (eglSwapBuffers(e_dpy, e_surf) == EGL_FALSE) {
|
||||
fprintf(stderr, "eglSwapBuffer failed\n");
|
||||
exit(1);
|
||||
}
|
||||
}
|
126
app/x11.go
Normal file
126
app/x11.go
Normal file
|
@ -0,0 +1,126 @@
|
|||
// Copyright 2014 The Go Authors. All rights reserved.
|
||||
// Use of this source code is governed by a BSD-style
|
||||
// license that can be found in the LICENSE file.
|
||||
|
||||
//go:build linux && !android
|
||||
|
||||
package app
|
||||
|
||||
/*
|
||||
Simple on-screen app debugging for X11. Not an officially supported
|
||||
development target for apps, as screens with mice are very different
|
||||
than screens with touch panels.
|
||||
*/
|
||||
|
||||
/*
|
||||
#cgo LDFLAGS: -lEGL -lGLESv2 -lX11
|
||||
|
||||
void createWindow(void);
|
||||
void processEvents(void);
|
||||
void swapBuffers(void);
|
||||
*/
|
||||
import "C"
|
||||
import (
|
||||
"runtime"
|
||||
"time"
|
||||
|
||||
"golang.org/x/mobile/event/lifecycle"
|
||||
"golang.org/x/mobile/event/paint"
|
||||
"golang.org/x/mobile/event/size"
|
||||
"golang.org/x/mobile/event/touch"
|
||||
"golang.org/x/mobile/geom"
|
||||
)
|
||||
|
||||
func init() {
|
||||
theApp.registerGLViewportFilter()
|
||||
}
|
||||
|
||||
func main(f func(App)) {
|
||||
runtime.LockOSThread()
|
||||
|
||||
workAvailable := theApp.worker.WorkAvailable()
|
||||
|
||||
C.createWindow()
|
||||
|
||||
// TODO: send lifecycle events when e.g. the X11 window is iconified or moved off-screen.
|
||||
theApp.sendLifecycle(lifecycle.StageFocused)
|
||||
|
||||
// TODO: translate X11 expose events to shiny paint events, instead of
|
||||
// sending this synthetic paint event as a hack.
|
||||
theApp.eventsIn <- paint.Event{}
|
||||
|
||||
donec := make(chan struct{})
|
||||
go func() {
|
||||
// close the donec channel in a defer statement
|
||||
// so that we could still be able to return even
|
||||
// if f panics.
|
||||
defer close(donec)
|
||||
|
||||
f(theApp)
|
||||
}()
|
||||
|
||||
// TODO: can we get the actual vsync signal?
|
||||
ticker := time.NewTicker(time.Second / 60)
|
||||
defer ticker.Stop()
|
||||
var tc <-chan time.Time
|
||||
|
||||
for {
|
||||
select {
|
||||
case <-donec:
|
||||
return
|
||||
case <-workAvailable:
|
||||
theApp.worker.DoWork()
|
||||
case <-theApp.publish:
|
||||
C.swapBuffers()
|
||||
tc = ticker.C
|
||||
case <-tc:
|
||||
tc = nil
|
||||
theApp.publishResult <- PublishResult{}
|
||||
}
|
||||
C.processEvents()
|
||||
}
|
||||
}
|
||||
|
||||
//export onResize
|
||||
func onResize(w, h int) {
|
||||
// TODO(nigeltao): don't assume 72 DPI. DisplayWidth and DisplayWidthMM
|
||||
// is probably the best place to start looking.
|
||||
pixelsPerPt := float32(1)
|
||||
theApp.eventsIn <- size.Event{
|
||||
WidthPx: w,
|
||||
HeightPx: h,
|
||||
WidthPt: geom.Pt(w),
|
||||
HeightPt: geom.Pt(h),
|
||||
PixelsPerPt: pixelsPerPt,
|
||||
}
|
||||
}
|
||||
|
||||
func sendTouch(t touch.Type, x, y float32) {
|
||||
theApp.eventsIn <- touch.Event{
|
||||
X: x,
|
||||
Y: y,
|
||||
Sequence: 0, // TODO: button??
|
||||
Type: t,
|
||||
}
|
||||
}
|
||||
|
||||
//export onTouchBegin
|
||||
func onTouchBegin(x, y float32) { sendTouch(touch.TypeBegin, x, y) }
|
||||
|
||||
//export onTouchMove
|
||||
func onTouchMove(x, y float32) { sendTouch(touch.TypeMove, x, y) }
|
||||
|
||||
//export onTouchEnd
|
||||
func onTouchEnd(x, y float32) { sendTouch(touch.TypeEnd, x, y) }
|
||||
|
||||
var stopped bool
|
||||
|
||||
//export onStop
|
||||
func onStop() {
|
||||
if stopped {
|
||||
return
|
||||
}
|
||||
stopped = true
|
||||
theApp.sendLifecycle(lifecycle.StageDead)
|
||||
theApp.eventsIn <- stopPumping{}
|
||||
}
|
Loading…
Add table
Add a link
Reference in a new issue