1
0
Fork 0

Adding upstream version 0.0~git20250520.a1d9079+dfsg.

Signed-off-by: Daniel Baumann <daniel@debian.org>
This commit is contained in:
Daniel Baumann 2025-05-24 19:46:29 +02:00
parent 590ac7ff5f
commit 20149b7f3a
Signed by: daniel
GPG key ID: FBB4F0E80A80222F
456 changed files with 70406 additions and 0 deletions

67
app/GoNativeActivity.java Normal file
View 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
View 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
View 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
View 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
View 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
View 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
View 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
View 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
View 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
View 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

View 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)
}
}

View 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)

View 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

View 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

View 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

View 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

View 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>

View 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
View 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
View 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
View 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{}
}