diff --git a/cmd/ebitenmobile/gobind.go b/cmd/ebitenmobile/gobind.go index 33c0581f6..a82ce1ed7 100644 --- a/cmd/ebitenmobile/gobind.go +++ b/cmd/ebitenmobile/gobind.go @@ -338,6 +338,7 @@ import android.os.Handler; import android.os.Looper; import android.util.AttributeSet; import android.util.Log; +import android.view.KeyEvent; import android.view.ViewGroup; import {{.JavaPkg}}.ebitenmobileview.Ebitenmobileview; @@ -354,10 +355,6 @@ public class EbitenView extends ViewGroup { return x / getDeviceScale(); } - private double dpToPx(double x) { - return x * getDeviceScale(); - } - private double deviceScale_ = 0.0; public EbitenView(Context context) { @@ -384,6 +381,18 @@ public class EbitenView extends ViewGroup { Ebitenmobileview.layout(widthInDp, heightInDp); } + @Override + public boolean onKeyDown(int keyCode, KeyEvent event) { + Ebitenmobileview.onKeyDownOnAndroid(keyCode); + return true; + } + + @Override + public boolean onKeyUp(int keyCode, KeyEvent event) { + Ebitenmobileview.onKeyUpOnAndroid(keyCode); + return true; + } + // suspendGame suspends the game. // It is recommended to call this when the application is being suspended e.g., // Activity's onPause is called. diff --git a/cmd/ebitenmobile/gobind.src.go b/cmd/ebitenmobile/gobind.src.go index f92cd8d59..e1e7d6bb8 100644 --- a/cmd/ebitenmobile/gobind.src.go +++ b/cmd/ebitenmobile/gobind.src.go @@ -3,4 +3,4 @@ package main -var gobindsrc = []byte("// Copyright 2019 The Ebiten Authors\n//\n// Licensed under the Apache License, Version 2.0 (the \"License\");\n// you may not use this file except in compliance with the License.\n// You may obtain a copy of the License at\n//\n// http://www.apache.org/licenses/LICENSE-2.0\n//\n// Unless required by applicable law or agreed to in writing, software\n// distributed under the License is distributed on an \"AS IS\" BASIS,\n// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n// See the License for the specific language governing permissions and\n// limitations under the License.\n\n// +build ebitenmobilegobind\n\n// gobind is a wrapper of the original gobind. This command adds extra files like a view controller.\npackage main\n\nimport (\n\t\"flag\"\n\t\"fmt\"\n\t\"io/ioutil\"\n\t\"log\"\n\t\"os\"\n\t\"os/exec\"\n\t\"path/filepath\"\n\t\"strings\"\n\n\t\"golang.org/x/tools/go/packages\"\n)\n\nvar (\n\tlang = flag.String(\"lang\", \"\", \"\")\n\toutdir = flag.String(\"outdir\", \"\", \"\")\n\tjavaPkg = flag.String(\"javapkg\", \"\", \"\")\n\tprefix = flag.String(\"prefix\", \"\", \"\")\n\tbootclasspath = flag.String(\"bootclasspath\", \"\", \"\")\n\tclasspath = flag.String(\"classpath\", \"\", \"\")\n\ttags = flag.String(\"tags\", \"\", \"\")\n)\n\nvar usage = `The Gobind tool generates Java language bindings for Go.\n\nFor usage details, see doc.go.`\n\nfunc main() {\n\tflag.Parse()\n\tif err := run(); err != nil {\n\t\tlog.Fatal(err)\n\t}\n}\n\nfunc invokeOriginalGobind(lang string) (pkgName string, err error) {\n\tcmd := exec.Command(\"gobind-original\", os.Args[1:]...)\n\tcmd.Stdout = os.Stdout\n\tcmd.Stderr = os.Stderr\n\tif err := cmd.Run(); err != nil {\n\t\treturn \"\", err\n\t}\n\n\tcfgtags := strings.Join(strings.Split(*tags, \",\"), \" \")\n\tcfg := &packages.Config{}\n\tswitch lang {\n\tcase \"java\":\n\t\tcfg.Env = append(os.Environ(), \"GOOS=android\")\n\tcase \"objc\":\n\t\tcfg.Env = append(os.Environ(), \"GOOS=darwin\")\n\t\tif cfgtags != \"\" {\n\t\t\tcfgtags += \" \"\n\t\t}\n\t\tcfgtags += \"ios\"\n\t}\n\tcfg.BuildFlags = []string{\"-tags\", cfgtags}\n\tpkgs, err := packages.Load(cfg, flag.Args()[0])\n\tif err != nil {\n\t\treturn \"\", err\n\t}\n\treturn pkgs[0].Name, nil\n}\n\nfunc forceGL() bool {\n\tfor _, tag := range strings.Split(*tags, \",\") {\n\t\tif tag == \"ebitengl\" {\n\t\t\treturn true\n\t\t}\n\t}\n\treturn false\n}\n\nfunc run() error {\n\twriteFile := func(filename string, content string) error {\n\t\tif err := ioutil.WriteFile(filepath.Join(*outdir, filename), []byte(content), 0644); err != nil {\n\t\t\treturn err\n\t\t}\n\t\treturn nil\n\t}\n\n\t// Add additional files.\n\tlangs := strings.Split(*lang, \",\")\n\tfor _, lang := range langs {\n\t\tpkgName, err := invokeOriginalGobind(lang)\n\t\tif err != nil {\n\t\t\treturn err\n\t\t}\n\t\tprefixLower := *prefix + pkgName\n\t\tprefixUpper := strings.Title(*prefix) + strings.Title(pkgName)\n\t\treplacePrefixes := func(content string) string {\n\t\t\tcontent = strings.ReplaceAll(content, \"{{.PrefixUpper}}\", prefixUpper)\n\t\t\tcontent = strings.ReplaceAll(content, \"{{.PrefixLower}}\", prefixLower)\n\t\t\tcontent = strings.ReplaceAll(content, \"{{.JavaPkg}}\", *javaPkg)\n\n\t\t\tf := \"0\"\n\t\t\tif forceGL() {\n\t\t\t\tf = \"1\"\n\t\t\t}\n\t\t\tcontent = strings.ReplaceAll(content, \"{{.ForceGL}}\", f)\n\t\t\treturn content\n\t\t}\n\n\t\tswitch lang {\n\t\tcase \"objc\":\n\t\t\t// iOS\n\t\t\tif err := writeFile(filepath.Join(\"src\", \"gobind\", prefixLower+\"ebitenviewcontroller_ios.m\"), replacePrefixes(objcM)); err != nil {\n\t\t\t\treturn err\n\t\t\t}\n\t\tcase \"java\":\n\t\t\t// Android\n\t\t\tdir := filepath.Join(strings.Split(*javaPkg, \".\")...)\n\t\t\tdir = filepath.Join(dir, prefixLower)\n\t\t\tif err := writeFile(filepath.Join(\"java\", dir, \"EbitenView.java\"), replacePrefixes(viewJava)); err != nil {\n\t\t\t\treturn err\n\t\t\t}\n\t\t\tif err := writeFile(filepath.Join(\"java\", dir, \"EbitenSurfaceView.java\"), replacePrefixes(surfaceViewJava)); err != nil {\n\t\t\t\treturn err\n\t\t\t}\n\t\tcase \"go\":\n\t\t\t// Do nothing.\n\t\tdefault:\n\t\t\tpanic(fmt.Sprintf(\"unsupported language: %s\", lang))\n\t\t}\n\t}\n\n\treturn nil\n}\n\nconst objcM = `// Code generated by ebitenmobile. DO NOT EDIT.\n\n// +build ios\n\n#import \n\n#if TARGET_IPHONE_SIMULATOR || {{.ForceGL}}\n#define EBITEN_METAL 0\n#else\n#define EBITEN_METAL 1\n#endif\n\n#import \n#import \n#import \n\n#import \"Ebitenmobileview.objc.h\"\n\n@interface {{.PrefixUpper}}EbitenViewController : UIViewController\n@end\n\n@implementation {{.PrefixUpper}}EbitenViewController {\n UIView* metalView_;\n GLKView* glkView_;\n bool started_;\n bool active_;\n bool error_;\n}\n\n- (UIView*)metalView {\n if (!metalView_) {\n metalView_ = [[UIView alloc] init];\n metalView_.multipleTouchEnabled = YES;\n }\n return metalView_;\n}\n\n- (GLKView*)glkView {\n if (!glkView_) {\n glkView_ = [[GLKView alloc] init];\n glkView_.multipleTouchEnabled = YES;\n }\n return glkView_;\n}\n\n- (void)viewDidLoad {\n [super viewDidLoad];\n\n if (!started_) {\n @synchronized(self) {\n active_ = true;\n }\n started_ = true;\n }\n\n#if EBITEN_METAL\n [self.view addSubview: self.metalView];\n EbitenmobileviewSetUIView((uintptr_t)(self.metalView));\n#else\n self.glkView.delegate = (id)(self);\n [self.view addSubview: self.glkView];\n\n EAGLContext *context = [[EAGLContext alloc] initWithAPI:kEAGLRenderingAPIOpenGLES2];\n [self glkView].context = context;\n\t\n [EAGLContext setCurrentContext:context];\n#endif\n\n CADisplayLink *displayLink = [CADisplayLink displayLinkWithTarget:self selector:@selector(drawFrame)];\n [displayLink addToRunLoop:[NSRunLoop currentRunLoop] forMode:NSDefaultRunLoopMode];\n}\n\n- (void)viewWillLayoutSubviews {\n CGRect viewRect = [[self view] frame];\n#if EBITEN_METAL\n [[self metalView] setFrame:viewRect];\n#else\n [[self glkView] setFrame:viewRect];\n#endif\n}\n\n- (void)viewDidLayoutSubviews {\n [super viewDidLayoutSubviews];\n CGRect viewRect = [[self view] frame];\n\n EbitenmobileviewLayout(viewRect.size.width, viewRect.size.height);\n}\n\n- (void)didReceiveMemoryWarning {\n [super didReceiveMemoryWarning];\n // Dispose of any resources that can be recreated.\n // TODO: Notify this to Go world?\n}\n\n- (void)drawFrame{\n @synchronized(self) {\n if (!active_) {\n return;\n }\n\n#if EBITEN_METAL\n [self updateEbiten];\n#else\n [[self glkView] setNeedsDisplay];\n#endif\n }\n}\n\n- (void)glkView:(GLKView*)view drawInRect:(CGRect)rect {\n @synchronized(self) {\n [self updateEbiten];\n }\n}\n\n- (void)updateEbiten {\n if (error_) {\n return;\n }\n NSError* err = nil;\n EbitenmobileviewUpdate(&err);\n if (err != nil) {\n [self performSelectorOnMainThread:@selector(onErrorOnGameUpdate:)\n withObject:err\n waitUntilDone:NO];\n error_ = true;\n }\n}\n\n- (void)onErrorOnGameUpdate:(NSError*)err {\n NSLog(@\"Error: %@\", err);\n}\n\n- (void)updateTouches:(NSSet*)touches {\n for (UITouch* touch in touches) {\n#if EBITEN_METAL\n if (touch.view != [self metalView]) {\n continue;\n }\n#else\n if (touch.view != [self glkView]) {\n continue;\n }\n#endif\n CGPoint location = [touch locationInView:touch.view];\n EbitenmobileviewUpdateTouchesOnIOS(touch.phase, (uintptr_t)touch, location.x, location.y);\n }\n}\n\n- (void)touchesBegan:(NSSet*)touches withEvent:(UIEvent*)event {\n [self updateTouches:touches];\n}\n\n- (void)touchesMoved:(NSSet*)touches withEvent:(UIEvent*)event {\n [self updateTouches:touches];\n}\n\n- (void)touchesEnded:(NSSet*)touches withEvent:(UIEvent*)event {\n [self updateTouches:touches];\n}\n\n- (void)touchesCancelled:(NSSet*)touches withEvent:(UIEvent*)event {\n [self updateTouches:touches];\n}\n\n- (void)suspendGame {\n NSAssert(started_, @\"suspendGame msut not be called before viewDidLoad is called\");\n\n @synchronized(self) {\n active_ = false;\n EbitenmobileviewSuspend();\n }\n}\n\n- (void)resumeGame {\n NSAssert(started_, @\"resumeGame msut not be called before viewDidLoad is called\");\n\n @synchronized(self) {\n active_ = true;\n EbitenmobileviewResume();\n }\n}\n\n@end\n`\n\nconst viewJava = `// Code generated by ebitenmobile. DO NOT EDIT.\n\npackage {{.JavaPkg}}.{{.PrefixLower}};\n\nimport android.content.Context;\nimport android.os.Handler;\nimport android.os.Looper;\nimport android.util.AttributeSet;\nimport android.util.Log;\nimport android.view.ViewGroup;\n\nimport {{.JavaPkg}}.ebitenmobileview.Ebitenmobileview;\n\npublic class EbitenView extends ViewGroup {\n private double getDeviceScale() {\n if (deviceScale_ == 0.0) {\n deviceScale_ = getResources().getDisplayMetrics().density;\n }\n return deviceScale_;\n }\n\n private double pxToDp(double x) {\n return x / getDeviceScale();\n }\n\n private double dpToPx(double x) {\n return x * getDeviceScale();\n }\n\n private double deviceScale_ = 0.0;\n\n public EbitenView(Context context) {\n super(context);\n initialize();\n }\n\n public EbitenView(Context context, AttributeSet attrs) {\n super(context, attrs);\n initialize();\n }\n\n private void initialize() {\n ebitenSurfaceView_ = new EbitenSurfaceView(getContext());\n LayoutParams params = new LayoutParams(LayoutParams.MATCH_PARENT, LayoutParams.MATCH_PARENT);\n addView(ebitenSurfaceView_, params);\n }\n\n @Override\n protected void onLayout(boolean changed, int left, int top, int right, int bottom) {\n ebitenSurfaceView_.layout(0, 0, right - left, bottom - top);\n double widthInDp = pxToDp(right - left);\n double heightInDp = pxToDp(bottom - top);\n Ebitenmobileview.layout(widthInDp, heightInDp);\n }\n\n // suspendGame suspends the game.\n // It is recommended to call this when the application is being suspended e.g.,\n // Activity's onPause is called.\n public void suspendGame() {\n ebitenSurfaceView_.onPause();\n Ebitenmobileview.suspend();\n }\n\n // resumeGame resumes the game.\n // It is recommended to call this when the application is being resumed e.g.,\n // Activity's onResume is called.\n public void resumeGame() {\n ebitenSurfaceView_.onResume();\n Ebitenmobileview.resume();\n }\n\n // onErrorOnGameUpdate is called on the main thread when an error happens when updating a game.\n // You can define your own error handler, e.g., using Crashlytics, by overwriting this method.\n protected void onErrorOnGameUpdate(Exception e) {\n Log.e(\"Go\", e.toString());\n }\n\n private EbitenSurfaceView ebitenSurfaceView_;\n}\n`\n\nconst surfaceViewJava = `// Code generated by ebitenmobile. DO NOT EDIT.\n\npackage {{.JavaPkg}}.{{.PrefixLower}};\n\nimport android.content.Context;\nimport android.opengl.GLSurfaceView;\nimport android.os.Handler;\nimport android.os.Looper;\nimport android.util.AttributeSet;\nimport android.view.MotionEvent;\n\nimport javax.microedition.khronos.egl.EGLConfig;\nimport javax.microedition.khronos.opengles.GL10;\n\nimport {{.JavaPkg}}.ebitenmobileview.Ebitenmobileview;\nimport {{.JavaPkg}}.{{.PrefixLower}}.EbitenView;\n\nclass EbitenSurfaceView extends GLSurfaceView {\n\n private class EbitenRenderer implements GLSurfaceView.Renderer {\n\n private boolean errored_ = false;\n\n @Override\n public void onDrawFrame(GL10 gl) {\n if (errored_) {\n return;\n }\n try {\n Ebitenmobileview.update();\n } catch (final Exception e) {\n new Handler(Looper.getMainLooper()).post(new Runnable() {\n @Override\n public void run() {\n onErrorOnGameUpdate(e);\n }\n });\n errored_ = true;\n }\n }\n\n @Override\n public void onSurfaceCreated(GL10 gl, EGLConfig config) {\n }\n\n @Override\n public void onSurfaceChanged(GL10 gl, int width, int height) {\n }\n }\n\n public EbitenSurfaceView(Context context) {\n super(context);\n initialize();\n }\n\n public EbitenSurfaceView(Context context, AttributeSet attrs) {\n super(context, attrs);\n initialize();\n }\n\n private void initialize() {\n setEGLContextClientVersion(2);\n setEGLConfigChooser(8, 8, 8, 8, 0, 0);\n setRenderer(new EbitenRenderer());\n }\n\n private double getDeviceScale() {\n if (deviceScale_ == 0.0) {\n deviceScale_ = getResources().getDisplayMetrics().density;\n }\n return deviceScale_;\n }\n\n private double pxToDp(double x) {\n return x / getDeviceScale();\n }\n\n @Override\n public boolean onTouchEvent(MotionEvent e) {\n for (int i = 0; i < e.getPointerCount(); i++) {\n int id = e.getPointerId(i);\n int x = (int)e.getX(i);\n int y = (int)e.getY(i);\n Ebitenmobileview.updateTouchesOnAndroid(e.getActionMasked(), id, (int)pxToDp(x), (int)pxToDp(y));\n }\n return true;\n }\n\n private void onErrorOnGameUpdate(Exception e) {\n ((EbitenView)getParent()).onErrorOnGameUpdate(e);\n }\n\n private double deviceScale_ = 0.0;\n}\n`\n") +var gobindsrc = []byte("// Copyright 2019 The Ebiten Authors\n//\n// Licensed under the Apache License, Version 2.0 (the \"License\");\n// you may not use this file except in compliance with the License.\n// You may obtain a copy of the License at\n//\n// http://www.apache.org/licenses/LICENSE-2.0\n//\n// Unless required by applicable law or agreed to in writing, software\n// distributed under the License is distributed on an \"AS IS\" BASIS,\n// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n// See the License for the specific language governing permissions and\n// limitations under the License.\n\n// +build ebitenmobilegobind\n\n// gobind is a wrapper of the original gobind. This command adds extra files like a view controller.\npackage main\n\nimport (\n\t\"flag\"\n\t\"fmt\"\n\t\"io/ioutil\"\n\t\"log\"\n\t\"os\"\n\t\"os/exec\"\n\t\"path/filepath\"\n\t\"strings\"\n\n\t\"golang.org/x/tools/go/packages\"\n)\n\nvar (\n\tlang = flag.String(\"lang\", \"\", \"\")\n\toutdir = flag.String(\"outdir\", \"\", \"\")\n\tjavaPkg = flag.String(\"javapkg\", \"\", \"\")\n\tprefix = flag.String(\"prefix\", \"\", \"\")\n\tbootclasspath = flag.String(\"bootclasspath\", \"\", \"\")\n\tclasspath = flag.String(\"classpath\", \"\", \"\")\n\ttags = flag.String(\"tags\", \"\", \"\")\n)\n\nvar usage = `The Gobind tool generates Java language bindings for Go.\n\nFor usage details, see doc.go.`\n\nfunc main() {\n\tflag.Parse()\n\tif err := run(); err != nil {\n\t\tlog.Fatal(err)\n\t}\n}\n\nfunc invokeOriginalGobind(lang string) (pkgName string, err error) {\n\tcmd := exec.Command(\"gobind-original\", os.Args[1:]...)\n\tcmd.Stdout = os.Stdout\n\tcmd.Stderr = os.Stderr\n\tif err := cmd.Run(); err != nil {\n\t\treturn \"\", err\n\t}\n\n\tcfgtags := strings.Join(strings.Split(*tags, \",\"), \" \")\n\tcfg := &packages.Config{}\n\tswitch lang {\n\tcase \"java\":\n\t\tcfg.Env = append(os.Environ(), \"GOOS=android\")\n\tcase \"objc\":\n\t\tcfg.Env = append(os.Environ(), \"GOOS=darwin\")\n\t\tif cfgtags != \"\" {\n\t\t\tcfgtags += \" \"\n\t\t}\n\t\tcfgtags += \"ios\"\n\t}\n\tcfg.BuildFlags = []string{\"-tags\", cfgtags}\n\tpkgs, err := packages.Load(cfg, flag.Args()[0])\n\tif err != nil {\n\t\treturn \"\", err\n\t}\n\treturn pkgs[0].Name, nil\n}\n\nfunc forceGL() bool {\n\tfor _, tag := range strings.Split(*tags, \",\") {\n\t\tif tag == \"ebitengl\" {\n\t\t\treturn true\n\t\t}\n\t}\n\treturn false\n}\n\nfunc run() error {\n\twriteFile := func(filename string, content string) error {\n\t\tif err := ioutil.WriteFile(filepath.Join(*outdir, filename), []byte(content), 0644); err != nil {\n\t\t\treturn err\n\t\t}\n\t\treturn nil\n\t}\n\n\t// Add additional files.\n\tlangs := strings.Split(*lang, \",\")\n\tfor _, lang := range langs {\n\t\tpkgName, err := invokeOriginalGobind(lang)\n\t\tif err != nil {\n\t\t\treturn err\n\t\t}\n\t\tprefixLower := *prefix + pkgName\n\t\tprefixUpper := strings.Title(*prefix) + strings.Title(pkgName)\n\t\treplacePrefixes := func(content string) string {\n\t\t\tcontent = strings.ReplaceAll(content, \"{{.PrefixUpper}}\", prefixUpper)\n\t\t\tcontent = strings.ReplaceAll(content, \"{{.PrefixLower}}\", prefixLower)\n\t\t\tcontent = strings.ReplaceAll(content, \"{{.JavaPkg}}\", *javaPkg)\n\n\t\t\tf := \"0\"\n\t\t\tif forceGL() {\n\t\t\t\tf = \"1\"\n\t\t\t}\n\t\t\tcontent = strings.ReplaceAll(content, \"{{.ForceGL}}\", f)\n\t\t\treturn content\n\t\t}\n\n\t\tswitch lang {\n\t\tcase \"objc\":\n\t\t\t// iOS\n\t\t\tif err := writeFile(filepath.Join(\"src\", \"gobind\", prefixLower+\"ebitenviewcontroller_ios.m\"), replacePrefixes(objcM)); err != nil {\n\t\t\t\treturn err\n\t\t\t}\n\t\tcase \"java\":\n\t\t\t// Android\n\t\t\tdir := filepath.Join(strings.Split(*javaPkg, \".\")...)\n\t\t\tdir = filepath.Join(dir, prefixLower)\n\t\t\tif err := writeFile(filepath.Join(\"java\", dir, \"EbitenView.java\"), replacePrefixes(viewJava)); err != nil {\n\t\t\t\treturn err\n\t\t\t}\n\t\t\tif err := writeFile(filepath.Join(\"java\", dir, \"EbitenSurfaceView.java\"), replacePrefixes(surfaceViewJava)); err != nil {\n\t\t\t\treturn err\n\t\t\t}\n\t\tcase \"go\":\n\t\t\t// Do nothing.\n\t\tdefault:\n\t\t\tpanic(fmt.Sprintf(\"unsupported language: %s\", lang))\n\t\t}\n\t}\n\n\treturn nil\n}\n\nconst objcM = `// Code generated by ebitenmobile. DO NOT EDIT.\n\n// +build ios\n\n#import \n\n#if TARGET_IPHONE_SIMULATOR || {{.ForceGL}}\n#define EBITEN_METAL 0\n#else\n#define EBITEN_METAL 1\n#endif\n\n#import \n#import \n#import \n\n#import \"Ebitenmobileview.objc.h\"\n\n@interface {{.PrefixUpper}}EbitenViewController : UIViewController\n@end\n\n@implementation {{.PrefixUpper}}EbitenViewController {\n UIView* metalView_;\n GLKView* glkView_;\n bool started_;\n bool active_;\n bool error_;\n}\n\n- (UIView*)metalView {\n if (!metalView_) {\n metalView_ = [[UIView alloc] init];\n metalView_.multipleTouchEnabled = YES;\n }\n return metalView_;\n}\n\n- (GLKView*)glkView {\n if (!glkView_) {\n glkView_ = [[GLKView alloc] init];\n glkView_.multipleTouchEnabled = YES;\n }\n return glkView_;\n}\n\n- (void)viewDidLoad {\n [super viewDidLoad];\n\n if (!started_) {\n @synchronized(self) {\n active_ = true;\n }\n started_ = true;\n }\n\n#if EBITEN_METAL\n [self.view addSubview: self.metalView];\n EbitenmobileviewSetUIView((uintptr_t)(self.metalView));\n#else\n self.glkView.delegate = (id)(self);\n [self.view addSubview: self.glkView];\n\n EAGLContext *context = [[EAGLContext alloc] initWithAPI:kEAGLRenderingAPIOpenGLES2];\n [self glkView].context = context;\n\t\n [EAGLContext setCurrentContext:context];\n#endif\n\n CADisplayLink *displayLink = [CADisplayLink displayLinkWithTarget:self selector:@selector(drawFrame)];\n [displayLink addToRunLoop:[NSRunLoop currentRunLoop] forMode:NSDefaultRunLoopMode];\n}\n\n- (void)viewWillLayoutSubviews {\n CGRect viewRect = [[self view] frame];\n#if EBITEN_METAL\n [[self metalView] setFrame:viewRect];\n#else\n [[self glkView] setFrame:viewRect];\n#endif\n}\n\n- (void)viewDidLayoutSubviews {\n [super viewDidLayoutSubviews];\n CGRect viewRect = [[self view] frame];\n\n EbitenmobileviewLayout(viewRect.size.width, viewRect.size.height);\n}\n\n- (void)didReceiveMemoryWarning {\n [super didReceiveMemoryWarning];\n // Dispose of any resources that can be recreated.\n // TODO: Notify this to Go world?\n}\n\n- (void)drawFrame{\n @synchronized(self) {\n if (!active_) {\n return;\n }\n\n#if EBITEN_METAL\n [self updateEbiten];\n#else\n [[self glkView] setNeedsDisplay];\n#endif\n }\n}\n\n- (void)glkView:(GLKView*)view drawInRect:(CGRect)rect {\n @synchronized(self) {\n [self updateEbiten];\n }\n}\n\n- (void)updateEbiten {\n if (error_) {\n return;\n }\n NSError* err = nil;\n EbitenmobileviewUpdate(&err);\n if (err != nil) {\n [self performSelectorOnMainThread:@selector(onErrorOnGameUpdate:)\n withObject:err\n waitUntilDone:NO];\n error_ = true;\n }\n}\n\n- (void)onErrorOnGameUpdate:(NSError*)err {\n NSLog(@\"Error: %@\", err);\n}\n\n- (void)updateTouches:(NSSet*)touches {\n for (UITouch* touch in touches) {\n#if EBITEN_METAL\n if (touch.view != [self metalView]) {\n continue;\n }\n#else\n if (touch.view != [self glkView]) {\n continue;\n }\n#endif\n CGPoint location = [touch locationInView:touch.view];\n EbitenmobileviewUpdateTouchesOnIOS(touch.phase, (uintptr_t)touch, location.x, location.y);\n }\n}\n\n- (void)touchesBegan:(NSSet*)touches withEvent:(UIEvent*)event {\n [self updateTouches:touches];\n}\n\n- (void)touchesMoved:(NSSet*)touches withEvent:(UIEvent*)event {\n [self updateTouches:touches];\n}\n\n- (void)touchesEnded:(NSSet*)touches withEvent:(UIEvent*)event {\n [self updateTouches:touches];\n}\n\n- (void)touchesCancelled:(NSSet*)touches withEvent:(UIEvent*)event {\n [self updateTouches:touches];\n}\n\n- (void)suspendGame {\n NSAssert(started_, @\"suspendGame msut not be called before viewDidLoad is called\");\n\n @synchronized(self) {\n active_ = false;\n EbitenmobileviewSuspend();\n }\n}\n\n- (void)resumeGame {\n NSAssert(started_, @\"resumeGame msut not be called before viewDidLoad is called\");\n\n @synchronized(self) {\n active_ = true;\n EbitenmobileviewResume();\n }\n}\n\n@end\n`\n\nconst viewJava = `// Code generated by ebitenmobile. DO NOT EDIT.\n\npackage {{.JavaPkg}}.{{.PrefixLower}};\n\nimport android.content.Context;\nimport android.os.Handler;\nimport android.os.Looper;\nimport android.util.AttributeSet;\nimport android.util.Log;\nimport android.view.KeyEvent;\nimport android.view.ViewGroup;\n\nimport {{.JavaPkg}}.ebitenmobileview.Ebitenmobileview;\n\npublic class EbitenView extends ViewGroup {\n private double getDeviceScale() {\n if (deviceScale_ == 0.0) {\n deviceScale_ = getResources().getDisplayMetrics().density;\n }\n return deviceScale_;\n }\n\n private double pxToDp(double x) {\n return x / getDeviceScale();\n }\n\n private double deviceScale_ = 0.0;\n\n public EbitenView(Context context) {\n super(context);\n initialize();\n }\n\n public EbitenView(Context context, AttributeSet attrs) {\n super(context, attrs);\n initialize();\n }\n\n private void initialize() {\n ebitenSurfaceView_ = new EbitenSurfaceView(getContext());\n LayoutParams params = new LayoutParams(LayoutParams.MATCH_PARENT, LayoutParams.MATCH_PARENT);\n addView(ebitenSurfaceView_, params);\n }\n\n @Override\n protected void onLayout(boolean changed, int left, int top, int right, int bottom) {\n ebitenSurfaceView_.layout(0, 0, right - left, bottom - top);\n double widthInDp = pxToDp(right - left);\n double heightInDp = pxToDp(bottom - top);\n Ebitenmobileview.layout(widthInDp, heightInDp);\n }\n\n @Override\n public boolean onKeyDown(int keyCode, KeyEvent event) {\n Ebitenmobileview.onKeyDownOnAndroid(keyCode);\n return true;\n }\n\n @Override\n public boolean onKeyUp(int keyCode, KeyEvent event) {\n Ebitenmobileview.onKeyUpOnAndroid(keyCode);\n return true;\n }\n\n // suspendGame suspends the game.\n // It is recommended to call this when the application is being suspended e.g.,\n // Activity's onPause is called.\n public void suspendGame() {\n ebitenSurfaceView_.onPause();\n Ebitenmobileview.suspend();\n }\n\n // resumeGame resumes the game.\n // It is recommended to call this when the application is being resumed e.g.,\n // Activity's onResume is called.\n public void resumeGame() {\n ebitenSurfaceView_.onResume();\n Ebitenmobileview.resume();\n }\n\n // onErrorOnGameUpdate is called on the main thread when an error happens when updating a game.\n // You can define your own error handler, e.g., using Crashlytics, by overwriting this method.\n protected void onErrorOnGameUpdate(Exception e) {\n Log.e(\"Go\", e.toString());\n }\n\n private EbitenSurfaceView ebitenSurfaceView_;\n}\n`\n\nconst surfaceViewJava = `// Code generated by ebitenmobile. DO NOT EDIT.\n\npackage {{.JavaPkg}}.{{.PrefixLower}};\n\nimport android.content.Context;\nimport android.opengl.GLSurfaceView;\nimport android.os.Handler;\nimport android.os.Looper;\nimport android.util.AttributeSet;\nimport android.view.MotionEvent;\n\nimport javax.microedition.khronos.egl.EGLConfig;\nimport javax.microedition.khronos.opengles.GL10;\n\nimport {{.JavaPkg}}.ebitenmobileview.Ebitenmobileview;\nimport {{.JavaPkg}}.{{.PrefixLower}}.EbitenView;\n\nclass EbitenSurfaceView extends GLSurfaceView {\n\n private class EbitenRenderer implements GLSurfaceView.Renderer {\n\n private boolean errored_ = false;\n\n @Override\n public void onDrawFrame(GL10 gl) {\n if (errored_) {\n return;\n }\n try {\n Ebitenmobileview.update();\n } catch (final Exception e) {\n new Handler(Looper.getMainLooper()).post(new Runnable() {\n @Override\n public void run() {\n onErrorOnGameUpdate(e);\n }\n });\n errored_ = true;\n }\n }\n\n @Override\n public void onSurfaceCreated(GL10 gl, EGLConfig config) {\n }\n\n @Override\n public void onSurfaceChanged(GL10 gl, int width, int height) {\n }\n }\n\n public EbitenSurfaceView(Context context) {\n super(context);\n initialize();\n }\n\n public EbitenSurfaceView(Context context, AttributeSet attrs) {\n super(context, attrs);\n initialize();\n }\n\n private void initialize() {\n setEGLContextClientVersion(2);\n setEGLConfigChooser(8, 8, 8, 8, 0, 0);\n setRenderer(new EbitenRenderer());\n }\n\n private double getDeviceScale() {\n if (deviceScale_ == 0.0) {\n deviceScale_ = getResources().getDisplayMetrics().density;\n }\n return deviceScale_;\n }\n\n private double pxToDp(double x) {\n return x / getDeviceScale();\n }\n\n @Override\n public boolean onTouchEvent(MotionEvent e) {\n for (int i = 0; i < e.getPointerCount(); i++) {\n int id = e.getPointerId(i);\n int x = (int)e.getX(i);\n int y = (int)e.getY(i);\n Ebitenmobileview.updateTouchesOnAndroid(e.getActionMasked(), id, (int)pxToDp(x), (int)pxToDp(y));\n }\n return true;\n }\n\n private void onErrorOnGameUpdate(Exception e) {\n ((EbitenView)getParent()).onErrorOnGameUpdate(e);\n }\n\n private double deviceScale_ = 0.0;\n}\n`\n") diff --git a/genkeys.go b/genkeys.go index 2d19ef2cf..79d931d1b 100644 --- a/genkeys.go +++ b/genkeys.go @@ -35,6 +35,7 @@ import ( var ( nameToGLFWKey map[string]glfw.Key + nameToAndroidKey map[string]int nameToJSKey map[string]string edgeKeyCodeToName map[int]string ) @@ -93,6 +94,56 @@ func init() { "KPEqual": glfw.KeyKPEqual, "Last": glfw.KeyLast, } + + // https://developer.android.com/reference/android/view/KeyEvent + nameToAndroidKey = map[string]int{ + "Comma": 55, + "Period": 56, + "LeftAlt": 57, + "RightAlt": 58, + "CapsLock": 115, + "LeftControl": 113, + "RightControl": 114, + "LeftShift": 59, + "RightShift": 60, + "Enter": 66, + "Space": 62, + "Tab": 61, + "Delete": 112, // KEYCODE_FORWARD_DEL + "End": 123, + "Home": 122, + "Insert": 124, + "PageDown": 93, + "PageUp": 92, + "Down": 20, + "Left": 21, + "Right": 22, + "Up": 19, + "Escape": 111, + "Backspace": 67, // KEYCODE_DEL + "Apostrophe": 75, + "Minus": 69, + "Slash": 76, + "Semicolon": 74, + "Equal": 70, + "LeftBracket": 71, + "Backslash": 73, + "RightBracket": 72, + "GraveAccent": 68, + "NumLock": 143, + "Pause": 121, // KEYCODE_BREAK + "PrintScreen": 120, // KEYCODE_SYSRQ + "ScrollLock": 116, + "Menu": 82, + "KPDecimal": 158, + "KPDivide": 154, + "KPMultiply": 155, + "KPSubtract": 156, + "KPAdd": 157, + "KPEnter": 160, + "KPEqual": 161, + } + nameToJSKey = map[string]string{ "Comma": "Comma", "Period": "Period", @@ -140,20 +191,24 @@ func init() { "KPEnter": "NumpadEnter", "KPEqual": "NumpadEqual", } + // ASCII: 0 - 9 for c := '0'; c <= '9'; c++ { nameToGLFWKey[string(c)] = glfw.Key0 + glfw.Key(c) - '0' + nameToAndroidKey[string(c)] = int(7 + c - '0') nameToJSKey[string(c)] = "Digit" + string(c) } // ASCII: A - Z for c := 'A'; c <= 'Z'; c++ { nameToGLFWKey[string(c)] = glfw.KeyA + glfw.Key(c) - 'A' + nameToAndroidKey[string(c)] = int(29 + c - 'A') nameToJSKey[string(c)] = "Key" + string(c) } // Function keys for i := 1; i <= 12; i++ { name := "F" + strconv.Itoa(i) nameToGLFWKey[name] = glfw.KeyF1 + glfw.Key(i) - 1 + nameToAndroidKey[name] = 131 + i - 1 nameToJSKey[name] = name } // Numpad @@ -161,6 +216,7 @@ func init() { for c := '0'; c <= '9'; c++ { name := "KP" + string(c) nameToGLFWKey[name] = glfw.KeyKP0 + glfw.Key(c) - '0' + nameToAndroidKey[name] = int(144 + c - '0') nameToJSKey[name] = "Numpad" + string(c) } } @@ -384,6 +440,24 @@ const ( ) ` +const mobileAndroidKeysTmpl = `{{.License}} + +{{.DoNotEdit}} + +{{.BuildTag}} + +package ebitenmobileview + +import ( + "github.com/hajimehoshi/ebiten/internal/driver" +) + +var androidKeyToDriverKey = map[int]driver.Key{ +{{range $name, $code := .NameToAndroidKey}}{{$code}}: driver.Key{{$name}}, +{{end}} +} +` + func digitKey(name string) int { if len(name) != 1 { return -1 @@ -502,12 +576,13 @@ func main() { sort.Slice(driverKeyNames, keyNamesLess(driverKeyNames)) for path, tmpl := range map[string]string{ - filepath.Join("event", "keys.go"): eventKeysTmpl, - filepath.Join("internal", "driver", "keys.go"): driverKeysTmpl, - filepath.Join("internal", "glfw", "keys.go"): glfwKeysTmpl, - filepath.Join("internal", "uidriver", "glfw", "keys.go"): uidriverGlfwKeysTmpl, - filepath.Join("internal", "uidriver", "js", "keys.go"): uidriverJsKeysTmpl, - filepath.Join("keys.go"): ebitenKeysTmpl, + filepath.Join("event", "keys.go"): eventKeysTmpl, + filepath.Join("internal", "driver", "keys.go"): driverKeysTmpl, + filepath.Join("internal", "glfw", "keys.go"): glfwKeysTmpl, + filepath.Join("internal", "uidriver", "glfw", "keys.go"): uidriverGlfwKeysTmpl, + filepath.Join("internal", "uidriver", "js", "keys.go"): uidriverJsKeysTmpl, + filepath.Join("keys.go"): ebitenKeysTmpl, + filepath.Join("mobile", "ebitenmobileview", "keys_android.go"): mobileAndroidKeysTmpl, } { f, err := os.Create(path) if err != nil { @@ -546,6 +621,7 @@ func main() { EbitenKeyNamesWithoutMods []string DriverKeyNames []string NameToGLFWKey map[string]glfw.Key + NameToAndroidKey map[string]int }{ License: license, DoNotEdit: doNotEdit, @@ -556,6 +632,7 @@ func main() { EbitenKeyNamesWithoutMods: ebitenKeyNamesWithoutMods, DriverKeyNames: driverKeyNames, NameToGLFWKey: nameToGLFWKey, + NameToAndroidKey: nameToAndroidKey, }); err != nil { log.Fatal(err) } diff --git a/input.go b/input.go index dc7cc1cb1..118397f40 100644 --- a/input.go +++ b/input.go @@ -39,6 +39,8 @@ func InputChars() []rune { // - KeyKPEnter and KeyKPEqual are recognized as KeyEnter and KeyEqual. // - KeyPrintScreen is only treated at keyup event. // +// On Android (ebitenmobile), EbitenView must be focusable to enable to handle keys. +// // IsKeyPressed is concurrent-safe. func IsKeyPressed(key Key) bool { // There are keys that are invalid values as ebiten.Key (e.g., driver.KeyLeftAlt). diff --git a/internal/uidriver/mobile/input.go b/internal/uidriver/mobile/input.go index 1fffbc123..70d7cb0f1 100644 --- a/internal/uidriver/mobile/input.go +++ b/internal/uidriver/mobile/input.go @@ -28,6 +28,7 @@ type pos struct { type Input struct { cursorX int cursorY int + keys map[driver.Key]struct{} touches map[int]pos ui *UserInterface } @@ -98,7 +99,14 @@ func (i *Input) RuneBuffer() []rune { } func (i *Input) IsKeyPressed(key driver.Key) bool { - return false + i.ui.m.RLock() + defer i.ui.m.RUnlock() + + if i.keys == nil { + return false + } + _, ok := i.keys[key] + return ok } func (i *Input) Wheel() (xoff, yoff float64) { @@ -109,8 +117,15 @@ func (i *Input) IsMouseButtonPressed(key driver.MouseButton) bool { return false } -func (i *Input) update(touches []*Touch) { +func (i *Input) update(keys map[driver.Key]struct{}, touches []*Touch) { i.ui.m.Lock() + defer i.ui.m.Unlock() + + i.keys = map[driver.Key]struct{}{} + for k := range keys { + i.keys[k] = struct{}{} + } + i.touches = map[int]pos{} for _, t := range touches { i.touches[t.ID] = pos{ @@ -118,7 +133,6 @@ func (i *Input) update(touches []*Touch) { Y: t.Y, } } - i.ui.m.Unlock() } func (i *Input) ResetForFrame() { diff --git a/internal/uidriver/mobile/ui.go b/internal/uidriver/mobile/ui.go index ef4fd13e8..740e758c8 100644 --- a/internal/uidriver/mobile/ui.go +++ b/internal/uidriver/mobile/ui.go @@ -199,7 +199,8 @@ func (u *UserInterface) appMain(a app.App) { for _, t := range touches { ts = append(ts, t) } - u.input.update(ts) + // TODO: Give keyboard keys. + u.input.update(nil, ts) } } } @@ -421,6 +422,6 @@ type Touch struct { Y int } -func (u *UserInterface) UpdateInput(touches []*Touch) { - u.input.update(touches) +func (u *UserInterface) UpdateInput(keys map[driver.Key]struct{}, touches []*Touch) { + u.input.update(keys, touches) } diff --git a/mobile/ebitenmobileview/input.go b/mobile/ebitenmobileview/input.go index 047e08af0..4d8c776d9 100644 --- a/mobile/ebitenmobileview/input.go +++ b/mobile/ebitenmobileview/input.go @@ -17,6 +17,7 @@ package ebitenmobileview import ( + "github.com/hajimehoshi/ebiten/internal/driver" "github.com/hajimehoshi/ebiten/internal/uidriver/mobile" ) @@ -26,6 +27,7 @@ type position struct { } var ( + keys = map[driver.Key]struct{}{} touches = map[int]position{} ) @@ -38,5 +40,5 @@ func updateInput() { Y: position.y, }) } - mobile.Get().UpdateInput(ts) + mobile.Get().UpdateInput(keys, ts) } diff --git a/mobile/ebitenmobileview/input_android.go b/mobile/ebitenmobileview/input_android.go index 842c082f7..424d0d122 100644 --- a/mobile/ebitenmobileview/input_android.go +++ b/mobile/ebitenmobileview/input_android.go @@ -30,3 +30,21 @@ func UpdateTouchesOnAndroid(action int, id int, x, y int) { func UpdateTouchesOnIOS(phase int, ptr int64, x, y int) { panic("ebitenmobileview: updateTouchesOnIOSImpl must not be called on Android") } + +func OnKeyDownOnAndroid(keyCode int) { + key, ok := androidKeyToDriverKey[keyCode] + if !ok { + return + } + keys[key] = struct{}{} + updateInput() +} + +func OnKeyUpOnAndroid(keyCode int) { + key, ok := androidKeyToDriverKey[keyCode] + if !ok { + return + } + delete(keys, key) + updateInput() +} diff --git a/mobile/ebitenmobileview/keys_android.go b/mobile/ebitenmobileview/keys_android.go new file mode 100644 index 000000000..8aa2831c5 --- /dev/null +++ b/mobile/ebitenmobileview/keys_android.go @@ -0,0 +1,127 @@ +// Copyright 2013 The Ebiten Authors +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +// Code generated by genkeys.go using 'go generate'. DO NOT EDIT. + +package ebitenmobileview + +import ( + "github.com/hajimehoshi/ebiten/internal/driver" +) + +var androidKeyToDriverKey = map[int]driver.Key{ + 7: driver.Key0, + 8: driver.Key1, + 9: driver.Key2, + 10: driver.Key3, + 11: driver.Key4, + 12: driver.Key5, + 13: driver.Key6, + 14: driver.Key7, + 15: driver.Key8, + 16: driver.Key9, + 29: driver.KeyA, + 75: driver.KeyApostrophe, + 30: driver.KeyB, + 73: driver.KeyBackslash, + 67: driver.KeyBackspace, + 31: driver.KeyC, + 115: driver.KeyCapsLock, + 55: driver.KeyComma, + 32: driver.KeyD, + 112: driver.KeyDelete, + 20: driver.KeyDown, + 33: driver.KeyE, + 123: driver.KeyEnd, + 66: driver.KeyEnter, + 70: driver.KeyEqual, + 111: driver.KeyEscape, + 34: driver.KeyF, + 131: driver.KeyF1, + 140: driver.KeyF10, + 141: driver.KeyF11, + 142: driver.KeyF12, + 132: driver.KeyF2, + 133: driver.KeyF3, + 134: driver.KeyF4, + 135: driver.KeyF5, + 136: driver.KeyF6, + 137: driver.KeyF7, + 138: driver.KeyF8, + 139: driver.KeyF9, + 35: driver.KeyG, + 68: driver.KeyGraveAccent, + 36: driver.KeyH, + 122: driver.KeyHome, + 37: driver.KeyI, + 124: driver.KeyInsert, + 38: driver.KeyJ, + 39: driver.KeyK, + 144: driver.KeyKP0, + 145: driver.KeyKP1, + 146: driver.KeyKP2, + 147: driver.KeyKP3, + 148: driver.KeyKP4, + 149: driver.KeyKP5, + 150: driver.KeyKP6, + 151: driver.KeyKP7, + 152: driver.KeyKP8, + 153: driver.KeyKP9, + 157: driver.KeyKPAdd, + 158: driver.KeyKPDecimal, + 154: driver.KeyKPDivide, + 160: driver.KeyKPEnter, + 161: driver.KeyKPEqual, + 155: driver.KeyKPMultiply, + 156: driver.KeyKPSubtract, + 40: driver.KeyL, + 21: driver.KeyLeft, + 57: driver.KeyLeftAlt, + 71: driver.KeyLeftBracket, + 113: driver.KeyLeftControl, + 59: driver.KeyLeftShift, + 41: driver.KeyM, + 82: driver.KeyMenu, + 69: driver.KeyMinus, + 42: driver.KeyN, + 143: driver.KeyNumLock, + 43: driver.KeyO, + 44: driver.KeyP, + 93: driver.KeyPageDown, + 92: driver.KeyPageUp, + 121: driver.KeyPause, + 56: driver.KeyPeriod, + 120: driver.KeyPrintScreen, + 45: driver.KeyQ, + 46: driver.KeyR, + 22: driver.KeyRight, + 58: driver.KeyRightAlt, + 72: driver.KeyRightBracket, + 114: driver.KeyRightControl, + 60: driver.KeyRightShift, + 47: driver.KeyS, + 116: driver.KeyScrollLock, + 74: driver.KeySemicolon, + 76: driver.KeySlash, + 62: driver.KeySpace, + 48: driver.KeyT, + 61: driver.KeyTab, + 49: driver.KeyU, + 19: driver.KeyUp, + 50: driver.KeyV, + 51: driver.KeyW, + 52: driver.KeyX, + 53: driver.KeyY, + 54: driver.KeyZ, +}