diff --git a/Common/System/System.h b/Common/System/System.h index ac22218c69..466bff7a2e 100644 --- a/Common/System/System.h +++ b/Common/System/System.h @@ -194,6 +194,8 @@ enum SystemProperty { SYSPROP_DISPLAY_SAFE_INSET_RIGHT, SYSPROP_DISPLAY_SAFE_INSET_TOP, SYSPROP_DISPLAY_SAFE_INSET_BOTTOM, + // How far down the screen the side insets are needed (they're for something in a top corner), or -1. + SYSPROP_DISPLAY_SIDE_INSETS_BOTTOM, SYSPROP_DISPLAY_HAS_CAMERA_CUTOUT, SYSPROP_DEVICE_TYPE, diff --git a/Common/UI/Context.cpp b/Common/UI/Context.cpp index 88a165d1f3..1e4832ebc7 100644 --- a/Common/UI/Context.cpp +++ b/Common/UI/Context.cpp @@ -162,6 +162,10 @@ Bounds UIContext::GetLayoutBounds(ViewLayoutMode layoutMode, bool immersiveMode) case ViewLayoutMode::IgnoreBottomInset: bottom = 0.0f; break; + case ViewLayoutMode::IgnoreSideInsets: + left = 0.0f; + right = 0.0f; + break; } // The side insets are only evened out in landscape. In portrait, a camera cutout is at the top, so a diff --git a/Common/UI/Screen.h b/Common/UI/Screen.h index c431a0a669..f42e3c50ad 100644 --- a/Common/UI/Screen.h +++ b/Common/UI/Screen.h @@ -74,6 +74,7 @@ enum class ViewLayoutMode { ApplyInsets = 0, // For gameplay, option #1 IgnoreInsets, // For gameplay, option #2 IgnoreBottomInset, + IgnoreSideInsets, }; enum class QueuedEventType : u8 { diff --git a/Common/UI/UIScreen.cpp b/Common/UI/UIScreen.cpp index 72a7b52201..3de6ea158e 100644 --- a/Common/UI/UIScreen.cpp +++ b/Common/UI/UIScreen.cpp @@ -45,6 +45,7 @@ void UIScreen::DoRecreateViews() { delete root_; root_ = nullptr; SetCreatingViews(true); + lastLayoutMode_ = LayoutMode(); CreateViews(); UI::View *defaultView = root_ ? root_->GetDefaultFocusView() : nullptr; if (defaultView && defaultView->GetVisibility() == UI::V_VISIBLE) { @@ -103,6 +104,10 @@ void UIScreen::update() { RecreateViews(); lastOrientation_ = orientation; } + // Views can be laid out for the bounds they were created in (like the touch controls). + if (LayoutMode() != lastLayoutMode_) { + RecreateViews(); + } DoRecreateViews(); diff --git a/Common/UI/UIScreen.h b/Common/UI/UIScreen.h index 42de88b561..2792bb567c 100644 --- a/Common/UI/UIScreen.h +++ b/Common/UI/UIScreen.h @@ -84,6 +84,7 @@ protected: bool recreateViews_ = true; DeviceOrientation lastOrientation_ = DeviceOrientation::Landscape; + ViewLayoutMode lastLayoutMode_ = ViewLayoutMode::ApplyInsets; }; class UIDialogScreen : public UIScreen { diff --git a/UI/EmuScreen.cpp b/UI/EmuScreen.cpp index dbe7db9b33..4fd09a796c 100644 --- a/UI/EmuScreen.cpp +++ b/UI/EmuScreen.cpp @@ -1418,7 +1418,7 @@ void EmuScreen::OpenChat(bool focus) { int GetChatMessageCount(); ViewLayoutMode EmuScreen::LayoutMode() const { - return ViewLayoutMode::ApplyInsets; + return TouchControlsLayoutMode(); } void EmuScreen::update() { @@ -2022,10 +2022,11 @@ void EmuScreen::renderUI() { if (PSP_IsInited()) { if ((DebugOverlay)g_Config.iDebugOverlay == DebugOverlay::CONTROL) { - DrawControlMapperOverlay(ctx, GetLayoutBounds(*ctx), g_controlMapper); + DrawControlMapperOverlay(ctx, ctx->GetLayoutBounds(ViewLayoutMode::ApplyInsets, UseImmersiveMode()), g_controlMapper); } if (g_Config.iShowStatusFlags) { - DrawFPS(ctx, GetLayoutBounds(*ctx)); + // Top right, so it can't skip the side insets like the controls can. + DrawFPS(ctx, ctx->GetLayoutBounds(ViewLayoutMode::ApplyInsets, UseImmersiveMode())); } } diff --git a/UI/GamepadEmu.cpp b/UI/GamepadEmu.cpp index 8e481b2e5d..388d0d48d7 100644 --- a/UI/GamepadEmu.cpp +++ b/UI/GamepadEmu.cpp @@ -998,6 +998,15 @@ void InitPadLayout(TouchControlConfig *config, DeviceOrientation orientation, fl } } +ViewLayoutMode TouchControlsLayoutMode() { + // The only control the defaults put in the top third is the centered menu button. + const float sideInsetsBottom = System_GetPropertyFloat(SYSPROP_DISPLAY_SIDE_INSETS_BOTTOM); + if (sideInsetsBottom >= 0.0f && sideInsetsBottom < g_display.dp_yres / 3.0f) { + return ViewLayoutMode::IgnoreSideInsets; + } + return ViewLayoutMode::ApplyInsets; +} + UI::ViewGroup *CreatePadLayout(const TouchControlConfig &config, float xres, float yres, bool *pause, ControlMapper *controlMapper) { using namespace UI; return new GamepadEmuView(config, xres, yres, pause, controlMapper, new UI::LayoutParams(UI::FILL_PARENT, UI::FILL_PARENT)); diff --git a/UI/GamepadEmu.h b/UI/GamepadEmu.h index 64598bfe72..165076562f 100644 --- a/UI/GamepadEmu.h +++ b/UI/GamepadEmu.h @@ -179,6 +179,8 @@ struct TouchControlConfig; // Initializes the layout from Config. if a default layout does not exist, it sets up default values void InitPadLayout(TouchControlConfig *config, DeviceOrientation orientation, float xres, float yres, float globalScale = 1.15f); +// Lets the controls use the full width when the side insets don't reach down to them. +ViewLayoutMode TouchControlsLayoutMode(); UI::ViewGroup *CreatePadLayout(const TouchControlConfig &config, float xres, float yres, bool *pause, ControlMapper *controlMapper); const int D_pad_Radius = 50; diff --git a/UI/TouchControlLayoutScreen.cpp b/UI/TouchControlLayoutScreen.cpp index cd845f2f4a..62a6398423 100644 --- a/UI/TouchControlLayoutScreen.cpp +++ b/UI/TouchControlLayoutScreen.cpp @@ -586,6 +586,10 @@ void TouchControlLayoutScreen::onFinish(DialogResult reason) { g_Config.Save("TouchControlLayoutScreen::onFinish"); } +ViewLayoutMode TouchControlLayoutScreen::LayoutMode() const { + return TouchControlsLayoutMode(); +} + void TouchControlLayoutScreen::OnReset(UI::EventParams &e) { INFO_LOG(Log::G3D, "Resetting touch control layout to default."); diff --git a/UI/TouchControlLayoutScreen.h b/UI/TouchControlLayoutScreen.h index 32fbc4ea2f..4ba8efe55d 100644 --- a/UI/TouchControlLayoutScreen.h +++ b/UI/TouchControlLayoutScreen.h @@ -39,7 +39,8 @@ public: const char *tag() const override { return "TouchControlLayout"; } protected: - ViewLayoutMode LayoutMode() const override { return ViewLayoutMode::ApplyInsets; } + // The same as EmuScreen's, so the controls go where they will be in the game. + ViewLayoutMode LayoutMode() const override; void OnReset(UI::EventParams &e); void OnMode(UI::EventParams &e); diff --git a/ios/ViewController.mm b/ios/ViewController.mm index 9092d98272..5c7e3d5bae 100644 --- a/ios/ViewController.mm +++ b/ios/ViewController.mm @@ -682,6 +682,7 @@ extern float g_safeInsetLeft; extern float g_safeInsetRight; extern float g_safeInsetTop; extern float g_safeInsetBottom; +extern float g_sideInsetsBottom; - (void)viewSafeAreaInsetsDidChange { [super viewSafeAreaInsetsDidChange]; @@ -724,6 +725,42 @@ extern float g_safeInsetBottom; g_safeInsetRight = std::max(g_safeInsetRight, cornerInset); } #endif + + const CGFloat sideInsetsBottom = [self sideInsetsBottom]; + g_sideInsetsBottom = sideInsetsBottom < 0.0 ? -1.0f : sideInsetsBottom * scale; +} + +// How far down the screen the side insets are needed, in points, or -1 if all the way. Side insets +// come from occlusions like the camera, and on the iPhone Duo's cover screen that's a box in the top +// right corner, which the touch controls don't need to avoid. +- (CGFloat)sideInsetsBottom { +#if __IPHONE_OS_VERSION_MAX_ALLOWED >= 270100 + if (@available(iOS 27.1, *)) { + UIEdgeInsets safe = self.view.safeAreaInsets; + if (safe.left <= 0.0 && safe.right <= 0.0) { + return -1.0; + } + const CGFloat width = self.view.bounds.size.width; + bool leftExplained = safe.left <= 0.0; + bool rightExplained = safe.right <= 0.0; + CGFloat bottom = 0.0; + for (UIViewReservedRegion *region in [self.view reservedRegionsOfKind:UIViewReservedRegionKind.occlusionRegionKind]) { + const CGRect frame = region.frame; + const bool onLeft = safe.left > 0.0 && CGRectGetMinX(frame) < safe.left; + const bool onRight = safe.right > 0.0 && CGRectGetMaxX(frame) > width - safe.right; + if (onLeft || onRight) { + leftExplained |= onLeft; + rightExplained |= onRight; + bottom = std::max(bottom, CGRectGetMaxY(frame)); + } + } + // An inset with no occlusion behind it (like the home indicator's) could be anywhere. + if (leftExplained && rightExplained) { + return bottom; + } + } +#endif + return -1.0; } - (void)shareText:(NSString *)text { @@ -861,13 +898,14 @@ static NSSet *NonKeyboardPresses(NSSet *presses) { // backbuffer matching the view while it animates. - (void)viewDidLayoutSubviews { [super viewDidLayoutSubviews]; + // A rotation doesn't always change the system's insets, but it can change ours. And the reserved + // regions can show up after the size and insets have settled. + [self updateSafeInsets]; CGSize size = self.view.bounds.size; if (size.width <= 0.0 || size.height <= 0.0 || CGSizeEqualToSize(size, lastLayoutSize_)) { return; } lastLayoutSize_ = size; - // A rotation doesn't always change the system's insets, but it can change ours. - [self updateSafeInsets]; #if !PPSSPP_PLATFORM(TVOS) if (self.view.window.windowScene) { [cameraHelper setInterfaceOrientation:self.view.window.windowScene.interfaceOrientation]; diff --git a/ios/main.mm b/ios/main.mm index 28ecf5affe..2ed28c15da 100644 --- a/ios/main.mm +++ b/ios/main.mm @@ -249,6 +249,7 @@ float g_safeInsetLeft = 0.0; float g_safeInsetRight = 0.0; float g_safeInsetTop = 0.0; float g_safeInsetBottom = 0.0; +float g_sideInsetsBottom = -1.0; static bool g_jitAvailable = true; @@ -322,6 +323,8 @@ float System_GetPropertyFloat(SystemProperty prop) { return g_safeInsetTop * g_display.dpi_scale_y; case SYSPROP_DISPLAY_SAFE_INSET_BOTTOM: return g_safeInsetBottom * g_display.dpi_scale_y; + case SYSPROP_DISPLAY_SIDE_INSETS_BOTTOM: + return g_sideInsetsBottom < 0.0f ? -1.0f : g_sideInsetsBottom * g_display.dpi_scale_y; default: return -1; }