From 9a8d6e7b41ac3638188d1ef22ee1eb662e78b579 Mon Sep 17 00:00:00 2001 From: Hajime Hoshi Date: Mon, 16 Sep 2024 17:42:05 +0900 Subject: [PATCH] internal/ui: implement (*Monitor).Size for mobiles Closes #2935 --- internal/ui/ui_android.go | 56 ++++++++++++++++++++++++++------------- internal/ui/ui_ios.go | 54 +++++++++++++++++++++++++++---------- internal/ui/ui_mobile.go | 37 ++++++++++++++++++-------- monitor.go | 5 ++-- 4 files changed, 105 insertions(+), 47 deletions(-) diff --git a/internal/ui/ui_android.go b/internal/ui/ui_android.go index ab71b61dd..ee1557ce3 100644 --- a/internal/ui/ui_android.go +++ b/internal/ui/ui_android.go @@ -18,15 +18,19 @@ package ui #include #include -// Basically same as: +// The following JNI code works as this pseudo Java code: // // WindowService windowService = context.getSystemService(Context.WINDOW_SERVICE); // Display display = windowManager.getDefaultDisplay(); // DisplayMetrics displayMetrics = new DisplayMetrics(); // display.getRealMetrics(displayMetrics); -// this.deviceScale = displayMetrics.density; +// return displayMetrics.widthPixels, displayMetrics.heightPixels, displayMetrics.density; // -static float deviceScale(uintptr_t java_vm, uintptr_t jni_env, uintptr_t ctx) { +static void displayInfo(int* width, int* height, float* scale, uintptr_t java_vm, uintptr_t jni_env, uintptr_t ctx) { + *width = 0; + *height = 0; + *scale = 1; + JavaVM* vm = (JavaVM*)java_vm; JNIEnv* env = (JNIEnv*)jni_env; jobject context = (jobject)ctx; @@ -64,7 +68,15 @@ static float deviceScale(uintptr_t java_vm, uintptr_t jni_env, uintptr_t ctx) { env, display, (*env)->GetMethodID(env, android_view_Display, "getRealMetrics", "(Landroid/util/DisplayMetrics;)V"), displayMetrics); - const float density = + *width = + (*env)->GetIntField( + env, displayMetrics, + (*env)->GetFieldID(env, android_util_DisplayMetrics, "widthPixels", "I")); + *height = + (*env)->GetIntField( + env, displayMetrics, + (*env)->GetFieldID(env, android_util_DisplayMetrics, "heightPixels", "I")); + *scale = (*env)->GetFloatField( env, displayMetrics, (*env)->GetFieldID(env, android_util_DisplayMetrics, "density", "F")); @@ -78,15 +90,12 @@ static float deviceScale(uintptr_t java_vm, uintptr_t jni_env, uintptr_t ctx) { (*env)->DeleteLocalRef(env, windowManager); (*env)->DeleteLocalRef(env, display); (*env)->DeleteLocalRef(env, displayMetrics); - - return density; } */ import "C" import ( "errors" - "fmt" "github.com/ebitengine/gomobile/app" @@ -119,18 +128,27 @@ func (*graphicsDriverCreatorImpl) newPlayStation5() (graphicsdriver.Graphics, er return nil, errors.New("ui: PlayStation 5 is not supported in this environment") } -func (u *UserInterface) deviceScaleFactor() float64 { - var s float64 - if err := app.RunOnJVM(func(vm, env, ctx uintptr) error { - // TODO: This might be crash when this is called from init(). How can we detect this? - s = float64(C.deviceScale(C.uintptr_t(vm), C.uintptr_t(env), C.uintptr_t(ctx))) - return nil - }); err != nil { - panic(fmt.Sprintf("devicescale: error %v", err)) - } - return s -} - func dipToNativePixels(x float64, scale float64) float64 { return x * scale } + +func dipFromNativePixels(x float64, scale float64) float64 { + return x / scale +} + +func (u *UserInterface) displayInfo() (int, int, float64, bool) { + var cWidth, cHeight C.int + var cScale C.float + if err := app.RunOnJVM(func(vm, env, ctx uintptr) error { + C.displayInfo(&cWidth, &cHeight, &cScale, C.uintptr_t(vm), C.uintptr_t(env), C.uintptr_t(ctx)) + return nil + }); err != nil { + // JVM is not ready yet. + // TODO: Fix gomobile to detect the error type for this case. + return 0, 0, 1, false + } + scale := float64(cScale) + width := int(dipFromNativePixels(float64(cWidth), scale)) + height := int(dipFromNativePixels(float64(cHeight), scale)) + return width, height, scale, true +} diff --git a/internal/ui/ui_ios.go b/internal/ui/ui_ios.go index 64760256e..15cbb3f57 100644 --- a/internal/ui/ui_ios.go +++ b/internal/ui/ui_ios.go @@ -19,31 +19,43 @@ package ui // // #import // -// static double devicePixelRatioOnMainThread(UIView* view) { +// static void displayInfoOnMainThread(float* width, float* height, float* scale, UIView* view) { +// *width = 0; +// *height = 0; +// *scale = 1; // UIWindow* window = view.window; // if (!window) { -// return 1; +// return; // } // UIWindowScene* scene = window.windowScene; // if (!scene) { -// return 1; +// return; // } -// return scene.screen.nativeScale; +// CGRect bounds = scene.screen.bounds; +// *width = bounds.size.width; +// *height = bounds.size.height; +// *scale = scene.screen.nativeScale; // } // -// static double devicePixelRatio(uintptr_t viewPtr) { +// static void displayInfo(float* width, float* height, float* scale, uintptr_t viewPtr) { +// *width = 0; +// *height = 0; +// *scale = 1; // if (!viewPtr) { -// return 1; +// return; // } // UIView* view = (__bridge UIView*)(void*)viewPtr; // if ([NSThread isMainThread]) { -// return devicePixelRatioOnMainThread(view); +// displayInfoOnMainThread(width, height, scale, view); +// return; // } -// __block double scale; +// __block float w, h, s; // dispatch_sync(dispatch_get_main_queue(), ^{ -// scale = devicePixelRatioOnMainThread(view); +// displayInfoOnMainThread(&w, &h, &s, view); // }); -// return scale; +// *width = w; +// *height = h; +// *scale = s; // } import "C" @@ -113,10 +125,24 @@ func (u *UserInterface) IsGL() (bool, error) { return u.GraphicsLibrary() == GraphicsLibraryOpenGL, nil } -func (u *UserInterface) deviceScaleFactor() float64 { - return float64(C.devicePixelRatio(C.uintptr_t(u.uiView.Load()))) -} - func dipToNativePixels(x float64, scale float64) float64 { return x } + +func dipFromNativePixels(x float64, scale float64) float64 { + return x +} + +func (u *UserInterface) displayInfo() (int, int, float64, bool) { + view := u.uiView.Load() + if view == 0 { + return 0, 0, 1, false + } + + var cWidth, cHeight, cScale C.float + C.displayInfo(&cWidth, &cHeight, &cScale, C.uintptr_t(view)) + scale := float64(cScale) + width := int(dipFromNativePixels(float64(cWidth), scale)) + height := int(dipFromNativePixels(float64(cHeight), scale)) + return width, height, scale, true +} diff --git a/internal/ui/ui_mobile.go b/internal/ui/ui_mobile.go index fd6a86038..7dc8f7efa 100644 --- a/internal/ui/ui_mobile.go +++ b/internal/ui/ui_mobile.go @@ -268,8 +268,10 @@ func (u *UserInterface) Window() Window { } type Monitor struct { - deviceScaleFactor float64 - deviceScaleFactorOnce sync.Once + width int + height int + deviceScaleFactor float64 + inited atomic.Bool m sync.Mutex } @@ -280,22 +282,35 @@ func (m *Monitor) Name() string { return "" } -func (m *Monitor) DeviceScaleFactor() float64 { +func (m *Monitor) ensureInit() { + if m.inited.Load() { + return + } + m.m.Lock() defer m.m.Unlock() + // Re-check the state since the state might be changed while locking. + if m.inited.Load() { + return + } + width, height, scale, ok := theUI.displayInfo() + if !ok { + return + } + m.width = width + m.height = height + m.deviceScaleFactor = scale + m.inited.Store(true) +} - // The device scale factor can be obtained after the main function starts, especially on Android. - // Initialize this lazily. - m.deviceScaleFactorOnce.Do(func() { - // Assume that the device scale factor never changes on mobiles. - m.deviceScaleFactor = theUI.deviceScaleFactor() - }) +func (m *Monitor) DeviceScaleFactor() float64 { + m.ensureInit() return m.deviceScaleFactor } func (m *Monitor) Size() (int, int) { - // TODO: Return a valid value. - return 0, 0 + m.ensureInit() + return m.width, m.height } func (u *UserInterface) AppendMonitors(mons []*Monitor) []*Monitor { diff --git a/monitor.go b/monitor.go index dfd7a93b3..c2827bf46 100644 --- a/monitor.go +++ b/monitor.go @@ -32,8 +32,7 @@ func (m *MonitorType) Name() string { // DeviceScaleFactor returns a meaningful value on high-DPI display environment, // otherwise DeviceScaleFactor returns 1. // -// DeviceScaleFactor might panic on init function on some devices like Android. -// Then, it is not recommended to call DeviceScaleFactor from init functions. +// On mobiles, DeviceScaleFactor returns 1 before the game starts e.g. in init functions. func (m *MonitorType) DeviceScaleFactor() float64 { return (*ui.Monitor)(m).DeviceScaleFactor() } @@ -42,7 +41,7 @@ func (m *MonitorType) DeviceScaleFactor() float64 { // This is the same as the screen size in fullscreen mode. // The returned value can be given to SetSize function if the perfectly fit fullscreen is needed. // -// On mobiles, Size returns (0, 0) so far. +// On mobiles, Size returns (0, 0) before the game starts e.g. in init functions. // // Size's use cases are limited. If you are making a fullscreen application, you can use RunGame and // the Game interface's Layout function instead. If you are making a not-fullscreen application but the application's