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 f2b4f2c51..a27ce19f2 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 74ecaf5bf..061775746 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 280c2a4a1..58467d3ae 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 2d3769ef9..3659c63ea 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 6f5e5ec6a..3e1b8b4f0 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 5b3b82556..2f3013f3b 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 22d210e9f..7318561b3 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 c7264154b..75d40beac 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 d166ced93..5cecaa3d3 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 0273b2c61..80fd511c1 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 2de6b74c9..2f12f8cd0 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 ec2eaba60..2be73c9b5 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 5f0caba9b..0f302b6b8 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 7b42c4944..eb5a50fba 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 0d720da72..20375d0f8 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 4f0c1f4d5..b01714535 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 {