forked from joejulian/gio
a3b9c7818f
Replace CreateWindow with NewWindow that immediately creates a Window ready to use. Drop the Windows channel of windows created by the system. For iOS and Android where the system creates the windows, let them rendezvous with the window created in the first NewWindow call. Android is further changed so that destroying and re-creating the Java Activity simply reconnects with the original Window. Signed-off-by: Elias Naur <mail@eliasnaur.com>
421 lines
9.5 KiB
Go
421 lines
9.5 KiB
Go
// SPDX-License-Identifier: Unlicense OR MIT
|
|
|
|
package app
|
|
|
|
/*
|
|
#cgo LDFLAGS: -landroid
|
|
|
|
#include <android/native_window_jni.h>
|
|
#include <android/configuration.h>
|
|
#include <android/keycodes.h>
|
|
#include <android/input.h>
|
|
#include <stdlib.h>
|
|
#include "os_android.h"
|
|
*/
|
|
import "C"
|
|
|
|
import (
|
|
"errors"
|
|
"fmt"
|
|
"image"
|
|
"runtime"
|
|
"runtime/debug"
|
|
"sync"
|
|
"time"
|
|
"unsafe"
|
|
|
|
"gioui.org/ui"
|
|
"gioui.org/ui/f32"
|
|
"gioui.org/ui/key"
|
|
"gioui.org/ui/pointer"
|
|
)
|
|
|
|
type window struct {
|
|
*Window
|
|
|
|
view C.jobject
|
|
|
|
dpi int
|
|
fontScale float32
|
|
insets Insets
|
|
|
|
stage Stage
|
|
started bool
|
|
|
|
mu sync.Mutex
|
|
win *C.ANativeWindow
|
|
animating bool
|
|
|
|
mgetDensity C.jmethodID
|
|
mgetFontScale C.jmethodID
|
|
mshowTextInput C.jmethodID
|
|
mhideTextInput C.jmethodID
|
|
mpostFrameCallback C.jmethodID
|
|
mpostFrameCallbackOnMainThread C.jmethodID
|
|
}
|
|
|
|
var theJVM *C.JavaVM
|
|
|
|
var views = make(map[C.jlong]*window)
|
|
|
|
var mainWindow = newWindowRendezvous()
|
|
|
|
func jniGetMethodID(env *C.JNIEnv, class C.jclass, method, sig string) C.jmethodID {
|
|
m := C.CString(method)
|
|
defer C.free(unsafe.Pointer(m))
|
|
s := C.CString(sig)
|
|
defer C.free(unsafe.Pointer(s))
|
|
return C.gio_jni_GetMethodID(env, class, m, s)
|
|
}
|
|
|
|
func jniGetStaticMethodID(env *C.JNIEnv, class C.jclass, method, sig string) C.jmethodID {
|
|
m := C.CString(method)
|
|
defer C.free(unsafe.Pointer(m))
|
|
s := C.CString(sig)
|
|
defer C.free(unsafe.Pointer(s))
|
|
return C.gio_jni_GetStaticMethodID(env, class, m, s)
|
|
}
|
|
|
|
//export setJVM
|
|
func setJVM(vm *C.JavaVM) {
|
|
theJVM = vm
|
|
}
|
|
|
|
//export onCreateView
|
|
func onCreateView(env *C.JNIEnv, class C.jclass, view C.jobject) C.jlong {
|
|
view = C.gio_jni_NewGlobalRef(env, view)
|
|
w := &window{
|
|
view: view,
|
|
mgetDensity: jniGetMethodID(env, class, "getDensity", "()I"),
|
|
mgetFontScale: jniGetMethodID(env, class, "getFontScale", "()F"),
|
|
mshowTextInput: jniGetMethodID(env, class, "showTextInput", "()V"),
|
|
mhideTextInput: jniGetMethodID(env, class, "hideTextInput", "()V"),
|
|
mpostFrameCallback: jniGetMethodID(env, class, "postFrameCallback", "()V"),
|
|
mpostFrameCallbackOnMainThread: jniGetMethodID(env, class, "postFrameCallbackOnMainThread", "()V"),
|
|
}
|
|
wopts := <-mainWindow.out
|
|
w.Window = wopts.window
|
|
w.Window.setDriver(w)
|
|
handle := C.jlong(view)
|
|
views[handle] = w
|
|
w.loadConfig(env, class)
|
|
w.setStage(StagePaused)
|
|
return handle
|
|
}
|
|
|
|
//export onDestroyView
|
|
func onDestroyView(env *C.JNIEnv, class C.jclass, handle C.jlong) {
|
|
w := views[handle]
|
|
w.setDriver(nil)
|
|
delete(views, handle)
|
|
C.gio_jni_DeleteGlobalRef(env, w.view)
|
|
w.view = 0
|
|
}
|
|
|
|
//export onStopView
|
|
func onStopView(env *C.JNIEnv, class C.jclass, handle C.jlong) {
|
|
w := views[handle]
|
|
w.started = false
|
|
w.setStage(StagePaused)
|
|
}
|
|
|
|
//export onStartView
|
|
func onStartView(env *C.JNIEnv, class C.jclass, handle C.jlong) {
|
|
w := views[handle]
|
|
w.started = true
|
|
if w.aNativeWindow() != nil {
|
|
w.setVisible()
|
|
}
|
|
}
|
|
|
|
//export onSurfaceDestroyed
|
|
func onSurfaceDestroyed(env *C.JNIEnv, class C.jclass, handle C.jlong) {
|
|
w := views[handle]
|
|
w.mu.Lock()
|
|
w.win = nil
|
|
w.mu.Unlock()
|
|
w.setStage(StagePaused)
|
|
}
|
|
|
|
//export onSurfaceChanged
|
|
func onSurfaceChanged(env *C.JNIEnv, class C.jclass, handle C.jlong, surf C.jobject) {
|
|
w := views[handle]
|
|
w.mu.Lock()
|
|
w.win = C.ANativeWindow_fromSurface(env, surf)
|
|
w.mu.Unlock()
|
|
if w.started {
|
|
w.setVisible()
|
|
}
|
|
}
|
|
|
|
//export onLowMemory
|
|
func onLowMemory() {
|
|
runtime.GC()
|
|
debug.FreeOSMemory()
|
|
}
|
|
|
|
//export onConfigurationChanged
|
|
func onConfigurationChanged(env *C.JNIEnv, class C.jclass, view C.jlong) {
|
|
w := views[view]
|
|
w.loadConfig(env, class)
|
|
if w.stage >= StageRunning {
|
|
w.draw(true)
|
|
}
|
|
}
|
|
|
|
//export onFrameCallback
|
|
func onFrameCallback(env *C.JNIEnv, class C.jclass, view C.jlong, nanos C.jlong) {
|
|
w, exist := views[view]
|
|
if !exist {
|
|
return
|
|
}
|
|
if w.stage < StageRunning {
|
|
return
|
|
}
|
|
w.mu.Lock()
|
|
anim := w.animating
|
|
w.mu.Unlock()
|
|
if anim {
|
|
runInJVM(func(env *C.JNIEnv) {
|
|
C.gio_jni_CallVoidMethod(env, w.view, w.mpostFrameCallback)
|
|
})
|
|
w.draw(false)
|
|
}
|
|
}
|
|
|
|
//export onBack
|
|
func onBack(env *C.JNIEnv, class C.jclass, view C.jlong) C.jboolean {
|
|
w := views[view]
|
|
ev := &CommandEvent{Type: CommandBack}
|
|
w.event(ev)
|
|
if ev.Cancel {
|
|
return C.JNI_TRUE
|
|
}
|
|
return C.JNI_FALSE
|
|
}
|
|
|
|
//export onFocusChange
|
|
func onFocusChange(env *C.JNIEnv, class C.jclass, view C.jlong, focus C.jboolean) {
|
|
w := views[view]
|
|
w.event(key.FocusEvent{Focus: focus == C.JNI_TRUE})
|
|
}
|
|
|
|
//export onWindowInsets
|
|
func onWindowInsets(env *C.JNIEnv, class C.jclass, view C.jlong, top, right, bottom, left C.jint) {
|
|
w := views[view]
|
|
w.insets = Insets{
|
|
Top: ui.Px(float32(top)),
|
|
Right: ui.Px(float32(right)),
|
|
Bottom: ui.Px(float32(bottom)),
|
|
Left: ui.Px(float32(left)),
|
|
}
|
|
if w.stage >= StageRunning {
|
|
w.draw(true)
|
|
}
|
|
}
|
|
|
|
func (w *window) setVisible() {
|
|
win := w.aNativeWindow()
|
|
width, height := C.ANativeWindow_getWidth(win), C.ANativeWindow_getHeight(win)
|
|
if width == 0 || height == 0 {
|
|
return
|
|
}
|
|
w.setStage(StageRunning)
|
|
w.draw(true)
|
|
}
|
|
|
|
func (w *window) setStage(stage Stage) {
|
|
if stage == w.stage {
|
|
return
|
|
}
|
|
w.stage = stage
|
|
w.event(StageEvent{stage})
|
|
}
|
|
|
|
func (w *window) display() unsafe.Pointer {
|
|
return nil
|
|
}
|
|
|
|
func (w *window) nativeWindow(visID int) (unsafe.Pointer, int, int) {
|
|
win := w.aNativeWindow()
|
|
var width, height int
|
|
if win != nil {
|
|
if C.ANativeWindow_setBuffersGeometry(win, 0, 0, C.int32_t(visID)) != 0 {
|
|
panic(errors.New("ANativeWindow_setBuffersGeometry failed"))
|
|
}
|
|
w, h := C.ANativeWindow_getWidth(win), C.ANativeWindow_getHeight(win)
|
|
width, height = int(w), int(h)
|
|
}
|
|
return unsafe.Pointer(win), width, height
|
|
}
|
|
|
|
func (w *window) aNativeWindow() *C.ANativeWindow {
|
|
w.mu.Lock()
|
|
defer w.mu.Unlock()
|
|
return w.win
|
|
}
|
|
|
|
func (w *window) loadConfig(env *C.JNIEnv, class C.jclass) {
|
|
dpi := int(C.gio_jni_CallIntMethod(env, w.view, w.mgetDensity))
|
|
w.fontScale = float32(C.gio_jni_CallFloatMethod(env, w.view, w.mgetFontScale))
|
|
switch dpi {
|
|
case C.ACONFIGURATION_DENSITY_NONE,
|
|
C.ACONFIGURATION_DENSITY_DEFAULT,
|
|
C.ACONFIGURATION_DENSITY_ANY:
|
|
// Assume standard density.
|
|
w.dpi = C.ACONFIGURATION_DENSITY_MEDIUM
|
|
default:
|
|
w.dpi = int(dpi)
|
|
}
|
|
}
|
|
|
|
func (w *window) setAnimating(anim bool) {
|
|
w.mu.Lock()
|
|
w.animating = anim
|
|
w.mu.Unlock()
|
|
if anim {
|
|
runInJVM(func(env *C.JNIEnv) {
|
|
C.gio_jni_CallVoidMethod(env, w.view, w.mpostFrameCallbackOnMainThread)
|
|
})
|
|
}
|
|
}
|
|
|
|
func (w *window) draw(sync bool) {
|
|
win := w.aNativeWindow()
|
|
width, height := C.ANativeWindow_getWidth(win), C.ANativeWindow_getHeight(win)
|
|
if width == 0 || height == 0 {
|
|
return
|
|
}
|
|
ppdp := float32(w.dpi) * inchPrDp
|
|
w.event(DrawEvent{
|
|
Size: image.Point{
|
|
X: int(width),
|
|
Y: int(height),
|
|
},
|
|
Insets: w.insets,
|
|
Config: Config{
|
|
pxPerDp: ppdp,
|
|
pxPerSp: w.fontScale * ppdp,
|
|
now: time.Now(),
|
|
},
|
|
sync: sync,
|
|
})
|
|
}
|
|
|
|
type keyMapper func(devId, keyCode C.int32_t) rune
|
|
|
|
func runInJVM(f func(env *C.JNIEnv)) {
|
|
runtime.LockOSThread()
|
|
defer runtime.UnlockOSThread()
|
|
var env *C.JNIEnv
|
|
var detach bool
|
|
if res := C.gio_jni_GetEnv(theJVM, &env, C.JNI_VERSION_1_6); res != C.JNI_OK {
|
|
if res != C.JNI_EDETACHED {
|
|
panic(fmt.Errorf("JNI GetEnv failed with error %d", res))
|
|
}
|
|
if C.gio_jni_AttachCurrentThread(theJVM, &env, nil) != C.JNI_OK {
|
|
panic(errors.New("runInJVM: AttachCurrentThread failed"))
|
|
}
|
|
detach = true
|
|
}
|
|
|
|
if detach {
|
|
defer func() {
|
|
C.gio_jni_DetachCurrentThread(theJVM)
|
|
}()
|
|
}
|
|
f(env)
|
|
}
|
|
|
|
func convertKeyCode(code C.jint) (rune, bool) {
|
|
var n rune
|
|
switch code {
|
|
case C.AKEYCODE_DPAD_UP:
|
|
n = key.NameUpArrow
|
|
case C.AKEYCODE_DPAD_DOWN:
|
|
n = key.NameDownArrow
|
|
case C.AKEYCODE_DPAD_LEFT:
|
|
n = key.NameLeftArrow
|
|
case C.AKEYCODE_DPAD_RIGHT:
|
|
n = key.NameRightArrow
|
|
case C.AKEYCODE_FORWARD_DEL:
|
|
n = key.NameDeleteForward
|
|
case C.AKEYCODE_DEL:
|
|
n = key.NameDeleteBackward
|
|
default:
|
|
return 0, false
|
|
}
|
|
return n, true
|
|
}
|
|
|
|
//export onKeyEvent
|
|
func onKeyEvent(env *C.JNIEnv, class C.jclass, handle C.jlong, keyCode, r C.jint, t C.jlong) {
|
|
w := views[handle]
|
|
if n, ok := convertKeyCode(keyCode); ok {
|
|
w.event(key.ChordEvent{Name: n})
|
|
}
|
|
if r != 0 {
|
|
w.event(key.EditEvent{Text: string(rune(r))})
|
|
}
|
|
}
|
|
|
|
//export onTouchEvent
|
|
func onTouchEvent(env *C.JNIEnv, class C.jclass, handle C.jlong, action, pointerID, tool C.jint, x, y C.jfloat, t C.jlong) {
|
|
w := views[handle]
|
|
var typ pointer.Type
|
|
switch action {
|
|
case C.AMOTION_EVENT_ACTION_DOWN, C.AMOTION_EVENT_ACTION_POINTER_DOWN:
|
|
typ = pointer.Press
|
|
case C.AMOTION_EVENT_ACTION_UP, C.AMOTION_EVENT_ACTION_POINTER_UP:
|
|
typ = pointer.Release
|
|
case C.AMOTION_EVENT_ACTION_CANCEL:
|
|
typ = pointer.Cancel
|
|
case C.AMOTION_EVENT_ACTION_MOVE:
|
|
typ = pointer.Move
|
|
default:
|
|
return
|
|
}
|
|
var src pointer.Source
|
|
switch tool {
|
|
case C.AMOTION_EVENT_TOOL_TYPE_FINGER:
|
|
src = pointer.Touch
|
|
case C.AMOTION_EVENT_TOOL_TYPE_MOUSE:
|
|
src = pointer.Mouse
|
|
default:
|
|
return
|
|
}
|
|
w.event(pointer.Event{
|
|
Type: typ,
|
|
Source: src,
|
|
PointerID: pointer.ID(pointerID),
|
|
Time: time.Duration(t) * time.Millisecond,
|
|
Position: f32.Point{X: float32(x), Y: float32(y)},
|
|
})
|
|
}
|
|
|
|
func (w *window) setTextInput(s key.TextInputState) {
|
|
if w.view == 0 {
|
|
return
|
|
}
|
|
switch s {
|
|
case key.TextInputOpen:
|
|
runInJVM(func(env *C.JNIEnv) {
|
|
C.gio_jni_CallVoidMethod(env, w.view, w.mshowTextInput)
|
|
})
|
|
case key.TextInputClose:
|
|
runInJVM(func(env *C.JNIEnv) {
|
|
C.gio_jni_CallVoidMethod(env, w.view, w.mhideTextInput)
|
|
})
|
|
}
|
|
}
|
|
|
|
func Main() {
|
|
// Android runs in c-shared mode where main is never reached.
|
|
panic("call to Main from outside main")
|
|
}
|
|
|
|
func createWindow(window *Window, opts *WindowOptions) error {
|
|
mainWindow.in <- windowAndOptions{window, opts}
|
|
return <-mainWindow.errs
|
|
}
|