From 457ca3995c9929cac3a3a617cdc4145d010d6a1c Mon Sep 17 00:00:00 2001 From: Sam Lantinga Date: Mon, 22 Jul 2024 18:59:53 -0700 Subject: [PATCH] Added SDL_GetWindowSafeArea() Fixes https://github.com/libsdl-org/SDL/issues/3243 --- .../main/java/org/libsdl/app/SDLActivity.java | 2 + .../main/java/org/libsdl/app/SDLSurface.java | 24 +++++++++++- include/SDL3/SDL_events.h | 1 + include/SDL3/SDL_video.h | 14 +++++++ src/core/android/SDL_android.c | 18 +++++++++ src/dynapi/SDL_dynapi.sym | 1 + src/dynapi/SDL_dynapi_overrides.h | 1 + src/dynapi/SDL_dynapi_procs.h | 1 + src/events/SDL_events.c | 1 + src/events/SDL_windowevents.c | 1 + src/test/SDL_test_common.c | 8 ++++ src/video/SDL_sysvideo.h | 7 ++++ src/video/SDL_video.c | 39 +++++++++++++++++++ src/video/cocoa/SDL_cocoawindow.m | 16 ++++++++ src/video/uikit/SDL_uikitview.h | 2 + src/video/uikit/SDL_uikitview.m | 12 ++++++ 16 files changed, 147 insertions(+), 1 deletion(-) diff --git a/android-project/app/src/main/java/org/libsdl/app/SDLActivity.java b/android-project/app/src/main/java/org/libsdl/app/SDLActivity.java index f2b4f2c516..a27ce19f2e 100644 --- a/android-project/app/src/main/java/org/libsdl/app/SDLActivity.java +++ b/android-project/app/src/main/java/org/libsdl/app/SDLActivity.java @@ -918,6 +918,7 @@ public class SDLActivity extends Activity implements View.OnSystemUiVisibilityCh window.clearFlags(WindowManager.LayoutParams.FLAG_FULLSCREEN); SDLActivity.mFullscreenModeActive = false; } + window.getAttributes().layoutInDisplayCutoutMode = WindowManager.LayoutParams.LAYOUT_IN_DISPLAY_CUTOUT_MODE_ALWAYS; } } else { Log.e(TAG, "error handling message, getContext() returned no Activity"); @@ -1057,6 +1058,7 @@ public class SDLActivity extends Activity implements View.OnSystemUiVisibilityCh public static native void nativeSetenv(String name, String value); public static native void nativeSetNaturalOrientation(int orientation); public static native void onNativeRotationChanged(int rotation); + public static native void onNativeInsetsChanged(int left, int right, int top, int bottom); public static native void nativeAddTouch(int touchId, String name); public static native void nativePermissionResult(int requestCode, boolean result); public static native void onNativeLocaleChanged(); diff --git a/android-project/app/src/main/java/org/libsdl/app/SDLSurface.java b/android-project/app/src/main/java/org/libsdl/app/SDLSurface.java index 74ecaf5bf4..0617757465 100644 --- a/android-project/app/src/main/java/org/libsdl/app/SDLSurface.java +++ b/android-project/app/src/main/java/org/libsdl/app/SDLSurface.java @@ -3,6 +3,7 @@ package org.libsdl.app; import android.content.Context; import android.content.pm.ActivityInfo; +import android.graphics.Insets; import android.hardware.Sensor; import android.hardware.SensorEvent; import android.hardware.SensorEventListener; @@ -18,6 +19,7 @@ import android.view.Surface; import android.view.SurfaceHolder; import android.view.SurfaceView; import android.view.View; +import android.view.WindowInsets; import android.view.WindowManager; @@ -28,7 +30,7 @@ import android.view.WindowManager; Because of this, that's where we set up the SDL thread */ public class SDLSurface extends SurfaceView implements SurfaceHolder.Callback, - View.OnKeyListener, View.OnTouchListener, SensorEventListener { + View.OnApplyWindowInsetsListener, View.OnKeyListener, View.OnTouchListener, SensorEventListener { // Sensors protected SensorManager mSensorManager; @@ -48,6 +50,7 @@ public class SDLSurface extends SurfaceView implements SurfaceHolder.Callback, setFocusable(true); setFocusableInTouchMode(true); requestFocus(); + setOnApplyWindowInsetsListener(this); setOnKeyListener(this); setOnTouchListener(this); @@ -71,6 +74,7 @@ public class SDLSurface extends SurfaceView implements SurfaceHolder.Callback, setFocusable(true); setFocusableInTouchMode(true); requestFocus(); + setOnApplyWindowInsetsListener(this); setOnKeyListener(this); setOnTouchListener(this); enableSensor(Sensor.TYPE_ACCELEROMETER, true); @@ -187,6 +191,24 @@ public class SDLSurface extends SurfaceView implements SurfaceHolder.Callback, SDLActivity.handleNativeState(); } + // Window inset + @Override + public WindowInsets onApplyWindowInsets(View v, WindowInsets insets) { + Insets combined = insets.getInsets(WindowInsets.Type.statusBars() | + WindowInsets.Type.navigationBars() | + WindowInsets.Type.captionBar() | + WindowInsets.Type.systemGestures() | + WindowInsets.Type.mandatorySystemGestures() | + WindowInsets.Type.tappableElement() | + WindowInsets.Type.displayCutout() | + WindowInsets.Type.systemOverlays()); + + SDLActivity.onNativeInsetsChanged(combined.left, combined.right, combined.top, combined.bottom); + + // Pass these to any child views in case they need them + return insets; + } + // Key events @Override public boolean onKey(View v, int keyCode, KeyEvent event) { diff --git a/include/SDL3/SDL_events.h b/include/SDL3/SDL_events.h index 280c2a4a1d..58467d3aec 100644 --- a/include/SDL3/SDL_events.h +++ b/include/SDL3/SDL_events.h @@ -142,6 +142,7 @@ typedef enum SDL_EventType SDL_EVENT_WINDOW_ICCPROF_CHANGED, /**< The ICC profile of the window's display has changed */ SDL_EVENT_WINDOW_DISPLAY_CHANGED, /**< Window has been moved to display data1 */ SDL_EVENT_WINDOW_DISPLAY_SCALE_CHANGED, /**< Window display scale has been changed */ + SDL_EVENT_WINDOW_SAFE_AREA_CHANGED, /**< The window safe area has been changed */ SDL_EVENT_WINDOW_OCCLUDED, /**< The window has been occluded */ SDL_EVENT_WINDOW_ENTER_FULLSCREEN, /**< The window has entered fullscreen mode */ SDL_EVENT_WINDOW_LEAVE_FULLSCREEN, /**< The window has left fullscreen mode */ diff --git a/include/SDL3/SDL_video.h b/include/SDL3/SDL_video.h index 2d3769ef99..3659c63ea9 100644 --- a/include/SDL3/SDL_video.h +++ b/include/SDL3/SDL_video.h @@ -1480,6 +1480,20 @@ extern SDL_DECLSPEC int SDLCALL SDL_SetWindowSize(SDL_Window *window, int w, int */ extern SDL_DECLSPEC int SDLCALL SDL_GetWindowSize(SDL_Window *window, int *w, int *h); +/** + * Get the safe area for this window. + * + * Some devices have portions of the screen which are partially obscured or not interactive, possibly due to on-screen controls, curved edges, camera notches, TV overscan, etc. This function provides the area of the window which is safe to have interactible content. You should continue rendering into the rest of the window, but it should not contain visually important or interactible content. + * + * \param window the window to query. + * \param rect a pointer filled in with the client area that is safe for interactive content. + * \returns 0 on success or a negative error code on failure; call + * SDL_GetError() for more information. + * + * \since This function is available since SDL 3.0.0. + */ +extern SDL_DECLSPEC int SDLCALL SDL_GetWindowSafeArea(SDL_Window *window, SDL_Rect *rect); + /** * Request that the aspect ratio of a window's client area be set. * diff --git a/src/core/android/SDL_android.c b/src/core/android/SDL_android.c index 6f5e5ec6a8..3e1b8b4f08 100644 --- a/src/core/android/SDL_android.c +++ b/src/core/android/SDL_android.c @@ -163,6 +163,10 @@ JNIEXPORT void JNICALL SDL_JAVA_INTERFACE(onNativeRotationChanged)( JNIEnv *env, jclass cls, jint rotation); +JNIEXPORT void JNICALL SDL_JAVA_INTERFACE(onNativeInsetsChanged)( + JNIEnv *env, jclass cls, + jint left, jint right, jint top, jint bottom); + JNIEXPORT void JNICALL SDL_JAVA_INTERFACE(nativeAddTouch)( JNIEnv *env, jclass cls, jint touchId, jstring name); @@ -212,6 +216,7 @@ static JNINativeMethod SDLActivity_tab[] = { { "nativeSetenv", "(Ljava/lang/String;Ljava/lang/String;)V", SDL_JAVA_INTERFACE(nativeSetenv) }, { "nativeSetNaturalOrientation", "(I)V", SDL_JAVA_INTERFACE(nativeSetNaturalOrientation) }, { "onNativeRotationChanged", "(I)V", SDL_JAVA_INTERFACE(onNativeRotationChanged) }, + { "onNativeInsetsChanged", "(IIII)V", SDL_JAVA_INTERFACE(onNativeInsetsChanged) }, { "nativeAddTouch", "(ILjava/lang/String;)V", SDL_JAVA_INTERFACE(nativeAddTouch) }, { "nativePermissionResult", "(IZ)V", SDL_JAVA_INTERFACE(nativePermissionResult) }, { "nativeAllowRecreateActivity", "()Z", SDL_JAVA_INTERFACE(nativeAllowRecreateActivity) }, @@ -997,6 +1002,19 @@ JNIEXPORT void JNICALL SDL_JAVA_INTERFACE(onNativeRotationChanged)( SDL_UnlockMutex(Android_ActivityMutex); } +JNIEXPORT void JNICALL SDL_JAVA_INTERFACE(onNativeInsetsChanged)( + JNIEnv *env, jclass jcls, + jint left, jint right, jint top, jint bottom) +{ + SDL_LockMutex(Android_ActivityMutex); + + if (Android_Window) { + SDL_SetWindowSafeAreaInsets(Android_Window, left, right, top, bottom); + } + + SDL_UnlockMutex(Android_ActivityMutex); +} + JNIEXPORT void JNICALL SDL_JAVA_INTERFACE(nativeAddTouch)( JNIEnv *env, jclass cls, jint touchId, jstring name) diff --git a/src/dynapi/SDL_dynapi.sym b/src/dynapi/SDL_dynapi.sym index 5b3b825566..2f3013f3bc 100644 --- a/src/dynapi/SDL_dynapi.sym +++ b/src/dynapi/SDL_dynapi.sym @@ -484,6 +484,7 @@ SDL3_0.0.0 { SDL_GetWindowPixelFormat; SDL_GetWindowPosition; SDL_GetWindowProperties; + SDL_GetWindowSafeArea; SDL_GetWindowSize; SDL_GetWindowSizeInPixels; SDL_GetWindowSurface; diff --git a/src/dynapi/SDL_dynapi_overrides.h b/src/dynapi/SDL_dynapi_overrides.h index 22d210e9f0..7318561b32 100644 --- a/src/dynapi/SDL_dynapi_overrides.h +++ b/src/dynapi/SDL_dynapi_overrides.h @@ -509,6 +509,7 @@ #define SDL_GetWindowPixelFormat SDL_GetWindowPixelFormat_REAL #define SDL_GetWindowPosition SDL_GetWindowPosition_REAL #define SDL_GetWindowProperties SDL_GetWindowProperties_REAL +#define SDL_GetWindowSafeArea SDL_GetWindowSafeArea_REAL #define SDL_GetWindowSize SDL_GetWindowSize_REAL #define SDL_GetWindowSizeInPixels SDL_GetWindowSizeInPixels_REAL #define SDL_GetWindowSurface SDL_GetWindowSurface_REAL diff --git a/src/dynapi/SDL_dynapi_procs.h b/src/dynapi/SDL_dynapi_procs.h index c7264154bf..75d40beac6 100644 --- a/src/dynapi/SDL_dynapi_procs.h +++ b/src/dynapi/SDL_dynapi_procs.h @@ -529,6 +529,7 @@ SDL_DYNAPI_PROC(float,SDL_GetWindowPixelDensity,(SDL_Window *a),(a),return) SDL_DYNAPI_PROC(SDL_PixelFormat,SDL_GetWindowPixelFormat,(SDL_Window *a),(a),return) SDL_DYNAPI_PROC(int,SDL_GetWindowPosition,(SDL_Window *a, int *b, int *c),(a,b,c),return) SDL_DYNAPI_PROC(SDL_PropertiesID,SDL_GetWindowProperties,(SDL_Window *a),(a),return) +SDL_DYNAPI_PROC(int,SDL_GetWindowSafeArea,(SDL_Window *a, SDL_Rect *b),(a,b),return) SDL_DYNAPI_PROC(int,SDL_GetWindowSize,(SDL_Window *a, int *b, int *c),(a,b,c),return) SDL_DYNAPI_PROC(int,SDL_GetWindowSizeInPixels,(SDL_Window *a, int *b, int *c),(a,b,c),return) SDL_DYNAPI_PROC(SDL_Surface*,SDL_GetWindowSurface,(SDL_Window *a),(a),return) diff --git a/src/events/SDL_events.c b/src/events/SDL_events.c index d166ced93b..5cecaa3d33 100644 --- a/src/events/SDL_events.c +++ b/src/events/SDL_events.c @@ -497,6 +497,7 @@ static void SDL_LogEvent(const SDL_Event *event) SDL_WINDOWEVENT_CASE(SDL_EVENT_WINDOW_MOVED); SDL_WINDOWEVENT_CASE(SDL_EVENT_WINDOW_RESIZED); SDL_WINDOWEVENT_CASE(SDL_EVENT_WINDOW_PIXEL_SIZE_CHANGED); + SDL_WINDOWEVENT_CASE(SDL_EVENT_WINDOW_SAFE_AREA_CHANGED); SDL_WINDOWEVENT_CASE(SDL_EVENT_WINDOW_MINIMIZED); SDL_WINDOWEVENT_CASE(SDL_EVENT_WINDOW_MAXIMIZED); SDL_WINDOWEVENT_CASE(SDL_EVENT_WINDOW_RESTORED); diff --git a/src/events/SDL_windowevents.c b/src/events/SDL_windowevents.c index 0273b2c61f..80fd511c15 100644 --- a/src/events/SDL_windowevents.c +++ b/src/events/SDL_windowevents.c @@ -193,6 +193,7 @@ int SDL_SendWindowEvent(SDL_Window *window, SDL_EventType windowevent, if (windowevent == SDL_EVENT_WINDOW_MOVED || windowevent == SDL_EVENT_WINDOW_RESIZED || windowevent == SDL_EVENT_WINDOW_PIXEL_SIZE_CHANGED || + windowevent == SDL_EVENT_WINDOW_SAFE_AREA_CHANGED || windowevent == SDL_EVENT_WINDOW_EXPOSED || windowevent == SDL_EVENT_WINDOW_OCCLUDED) { SDL_FilterEvents(RemoveSupercededWindowEvents, &event); diff --git a/src/test/SDL_test_common.c b/src/test/SDL_test_common.c index 2de6b74c98..2f12f8cd0b 100644 --- a/src/test/SDL_test_common.c +++ b/src/test/SDL_test_common.c @@ -1614,6 +1614,14 @@ static void SDLTest_PrintEvent(const SDL_Event *event) SDL_Log("SDL EVENT: Window %" SDL_PRIu32 " changed pixel size to %" SDL_PRIs32 "x%" SDL_PRIs32, event->window.windowID, event->window.data1, event->window.data2); break; + case SDL_EVENT_WINDOW_SAFE_AREA_CHANGED: { + SDL_Rect rect; + + SDL_GetWindowSafeArea(SDL_GetWindowFromID(event->window.windowID), &rect); + SDL_Log("SDL EVENT: Window %" SDL_PRIu32 " changed safe area to: %d,%d %dx%d\n", + event->window.windowID, rect.x, rect.y, rect.w, rect.h); + break; + } case SDL_EVENT_WINDOW_MINIMIZED: SDL_Log("SDL EVENT: Window %" SDL_PRIu32 " minimized", event->window.windowID); break; diff --git a/src/video/SDL_sysvideo.h b/src/video/SDL_sysvideo.h index ec2eaba606..2be73c9b56 100644 --- a/src/video/SDL_sysvideo.h +++ b/src/video/SDL_sysvideo.h @@ -100,6 +100,12 @@ struct SDL_Window SDL_bool is_destroying; SDL_bool is_dropping; /* drag/drop in progress, expecting SDL_SendDropComplete(). */ + int safe_inset_left; + int safe_inset_right; + int safe_inset_top; + int safe_inset_bottom; + SDL_Rect safe_rect; + SDL_bool text_input_active; SDL_Rect text_input_rect; int text_input_cursor; @@ -526,6 +532,7 @@ extern SDL_DisplayData *SDL_GetDisplayDriverData(SDL_DisplayID display); extern SDL_DisplayData *SDL_GetDisplayDriverDataForWindow(SDL_Window *window); extern int SDL_GetMessageBoxCount(void); extern void SDL_SetWindowHDRProperties(SDL_Window *window, const SDL_HDROutputProperties *HDR, SDL_bool send_event); +extern void SDL_SetWindowSafeAreaInsets(SDL_Window *window, int left, int right, int top, int bottom); extern void SDL_GL_DeduceMaxSupportedESProfile(int *major, int *minor); diff --git a/src/video/SDL_video.c b/src/video/SDL_video.c index 5f0caba9b5..0f302b6b82 100644 --- a/src/video/SDL_video.c +++ b/src/video/SDL_video.c @@ -165,6 +165,7 @@ extern SDL_bool Cocoa_SetWindowFullscreenSpace(SDL_Window *window, SDL_bool stat static void SDL_CheckWindowDisplayChanged(SDL_Window *window); static void SDL_CheckWindowDisplayScaleChanged(SDL_Window *window); +static void SDL_CheckWindowSafeAreaChanged(SDL_Window *window); /* Convenience functions for reading driver flags */ static SDL_bool SDL_ModeSwitchingEmulated(SDL_VideoDevice *_this) @@ -3789,6 +3790,7 @@ void SDL_OnWindowResized(SDL_Window *window) { SDL_CheckWindowDisplayChanged(window); SDL_CheckWindowPixelSizeChanged(window); + SDL_CheckWindowSafeAreaChanged(window); if ((window->flags & SDL_WINDOW_TRANSPARENT) && _this->UpdateWindowShape) { SDL_Surface *surface = (SDL_Surface *)SDL_GetPointerProperty(window->props, SDL_PROP_WINDOW_SHAPE_POINTER, NULL); @@ -3813,6 +3815,43 @@ void SDL_OnWindowPixelSizeChanged(SDL_Window *window) window->surface_valid = SDL_FALSE; } +static void SDL_CheckWindowSafeAreaChanged(SDL_Window *window) +{ + SDL_Rect rect; + + rect.x = window->safe_inset_left; + rect.y = window->safe_inset_top; + rect.w = window->w - (window->safe_inset_right + window->safe_inset_left); + rect.h = window->h - (window->safe_inset_top + window->safe_inset_bottom); + if (SDL_memcmp(&rect, &window->safe_rect, sizeof(rect)) != 0) { + SDL_copyp(&window->safe_rect, &rect); + SDL_SendWindowEvent(window, SDL_EVENT_WINDOW_SAFE_AREA_CHANGED, 0, 0); + } +} + +void SDL_SetWindowSafeAreaInsets(SDL_Window *window, int left, int right, int top, int bottom) +{ + window->safe_inset_left = left; + window->safe_inset_right = right; + window->safe_inset_top = top; + window->safe_inset_bottom = bottom; + SDL_CheckWindowSafeAreaChanged(window); +} + +int SDL_GetWindowSafeArea(SDL_Window *window, SDL_Rect *rect) +{ + if (rect) { + SDL_zerop(rect); + } + + CHECK_WINDOW_MAGIC(window, -1); + + if (rect) { + SDL_copyp(rect, &window->safe_rect); + } + return 0; +} + void SDL_OnWindowMinimized(SDL_Window *window) { if (window->flags & SDL_WINDOW_FULLSCREEN) { diff --git a/src/video/cocoa/SDL_cocoawindow.m b/src/video/cocoa/SDL_cocoawindow.m index 7b42c49443..eb5a50fbab 100644 --- a/src/video/cocoa/SDL_cocoawindow.m +++ b/src/video/cocoa/SDL_cocoawindow.m @@ -2785,6 +2785,22 @@ int Cocoa_SetWindowFullscreen(SDL_VideoDevice *_this, SDL_Window *window, SDL_Vi [data.listener resumeVisibleObservation]; } + // Update the safe area insets + // The view never seems to reflect the safe area, so we'll use the screen instead + if (@available(macOS 12.0, *)) { + if (fullscreen) { + NSScreen *screen = [nswindow screen]; + + SDL_SetWindowSafeAreaInsets(data.window, + (int)SDL_ceilf(screen.safeAreaInsets.left), + (int)SDL_ceilf(screen.safeAreaInsets.right), + (int)SDL_ceilf(screen.safeAreaInsets.top), + (int)SDL_ceilf(screen.safeAreaInsets.bottom)); + } else { + SDL_SetWindowSafeAreaInsets(data.window, 0, 0, 0, 0); + } + } + ScheduleContextUpdates(data); } diff --git a/src/video/uikit/SDL_uikitview.h b/src/video/uikit/SDL_uikitview.h index 0d720da720..20375d0f8e 100644 --- a/src/video/uikit/SDL_uikitview.h +++ b/src/video/uikit/SDL_uikitview.h @@ -43,4 +43,6 @@ - (void)touchesEnded:(NSSet *)touches withEvent:(UIEvent *)event; - (void)touchesMoved:(NSSet *)touches withEvent:(UIEvent *)event; +- (void)safeAreaInsetsDidChange; + @end diff --git a/src/video/uikit/SDL_uikitview.m b/src/video/uikit/SDL_uikitview.m index 4f0c1f4d5c..b01714535f 100644 --- a/src/video/uikit/SDL_uikitview.m +++ b/src/video/uikit/SDL_uikitview.m @@ -368,6 +368,18 @@ extern int SDL_AppleTVRemoteOpenedAsJoystick; } } +- (void)safeAreaInsetsDidChange +{ + // Update the safe area insets + if (@available(iOS 11.0, tvOS 11.0, *)) { + SDL_SetWindowSafeAreaInsets(sdlwindow, + (int)SDL_ceilf(self.safeAreaInsets.left), + (int)SDL_ceilf(self.safeAreaInsets.right), + (int)SDL_ceilf(self.safeAreaInsets.top), + (int)SDL_ceilf(self.safeAreaInsets.bottom)); + } +} + #if defined(SDL_PLATFORM_TVOS) || defined(__IPHONE_9_1) - (SDL_Scancode)scancodeFromPress:(UIPress *)press {