From 858746350299ab6e55cb64597e34d00ec53494a9 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Henrik=20Rydg=C3=A5rd?= Date: Mon, 9 Nov 2026 10:36:30 +0100 Subject: [PATCH 1/2] UI: Keep the pause screen out of a hidden navigation bar's area With the navigation bar hidden, a swipe from the edge brings it back for a while, over the pause screen's buttons on that edge. Android now also reports where the navigation bar is when it's hidden, and a screen can reserve that area with ReserveHiddenNavBar(). The pause screen does. Co-Authored-By: Claude Opus 5.5 --- Common/System/System.h | 5 +++++ Common/UI/Context.cpp | 14 ++++++++++++- Common/UI/Context.h | 3 ++- Common/UI/UIScreen.cpp | 4 ++-- Common/UI/UIScreen.h | 2 ++ UI/PauseScreen.h | 1 + android/jni/app-android.cpp | 20 +++++++++++++++++-- .../src/org/ppsspp/ppsspp/PpssppActivity.java | 5 ++++- 8 files changed, 47 insertions(+), 7 deletions(-) diff --git a/Common/System/System.h b/Common/System/System.h index 085bc947f0..3d15195ca2 100644 --- a/Common/System/System.h +++ b/Common/System/System.h @@ -200,6 +200,11 @@ enum SystemProperty { // 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, + // The navigation bar's insets, also while it's hidden (a swipe from the edge brings it back). + SYSPROP_DISPLAY_NAVBAR_INSET_LEFT, + SYSPROP_DISPLAY_NAVBAR_INSET_RIGHT, + SYSPROP_DISPLAY_NAVBAR_INSET_TOP, + SYSPROP_DISPLAY_NAVBAR_INSET_BOTTOM, SYSPROP_DEVICE_TYPE, SYSPROP_APP_GOLD, // To avoid having #ifdef GOLD other than in main.cpp and similar. diff --git a/Common/UI/Context.cpp b/Common/UI/Context.cpp index a131ee4f07..030af9206d 100644 --- a/Common/UI/Context.cpp +++ b/Common/UI/Context.cpp @@ -146,7 +146,7 @@ Bounds UIContext::GetScissorBounds() { return bounds_; } -Bounds UIContext::GetLayoutBounds(ViewLayoutMode layoutMode, bool immersiveMode) const { +Bounds UIContext::GetLayoutBounds(ViewLayoutMode layoutMode, bool immersiveMode, bool reserveHiddenNavBar) const { float left = System_GetPropertyFloat(SYSPROP_DISPLAY_SAFE_INSET_LEFT); float right = System_GetPropertyFloat(SYSPROP_DISPLAY_SAFE_INSET_RIGHT); float top = System_GetPropertyFloat(SYSPROP_DISPLAY_SAFE_INSET_TOP); @@ -205,6 +205,18 @@ Bounds UIContext::GetLayoutBounds(ViewLayoutMode layoutMode, bool immersiveMode) } } + if (reserveHiddenNavBar && layoutMode != ViewLayoutMode::IgnoreInsets) { + // Platforms without a navigation bar return -1, which leaves the insets alone. + if (layoutMode != ViewLayoutMode::IgnoreSideInsets) { + left = std::max(left, System_GetPropertyFloat(SYSPROP_DISPLAY_NAVBAR_INSET_LEFT)); + right = std::max(right, System_GetPropertyFloat(SYSPROP_DISPLAY_NAVBAR_INSET_RIGHT)); + } + top = std::max(top, System_GetPropertyFloat(SYSPROP_DISPLAY_NAVBAR_INSET_TOP)); + if (layoutMode != ViewLayoutMode::IgnoreBottomInset) { + bottom = std::max(bottom, System_GetPropertyFloat(SYSPROP_DISPLAY_NAVBAR_INSET_BOTTOM)); + } + } + // Note that we ignore bottom here, to let lists etc. extend to the bottom of the screen. // However, we will add the same space to the bottom of scrolled lists, so that you don't have to // touch things below the safe inset. diff --git a/Common/UI/Context.h b/Common/UI/Context.h index 4e783b3101..996420d09e 100644 --- a/Common/UI/Context.h +++ b/Common/UI/Context.h @@ -118,7 +118,8 @@ public: // in dps, like dp_xres and dp_yres void SetBounds(const Bounds &b) { bounds_ = b; } const Bounds &GetBounds() const { return bounds_; } - Bounds GetLayoutBounds(ViewLayoutMode layoutMode, bool immersiveMode) const; + // reserveHiddenNavBar keeps out of where the navigation bar comes back on a swipe, even while it's hidden. + Bounds GetLayoutBounds(ViewLayoutMode layoutMode, bool immersiveMode, bool reserveHiddenNavBar = false) const; Draw::DrawContext *GetDrawContext() const { return draw_; } const UI::Theme &GetTheme() const { return *theme; diff --git a/Common/UI/UIScreen.cpp b/Common/UI/UIScreen.cpp index 3de6ea158e..a93103ae2f 100644 --- a/Common/UI/UIScreen.cpp +++ b/Common/UI/UIScreen.cpp @@ -130,7 +130,7 @@ void UIScreen::deviceRestored(Draw::DrawContext *draw) { } Bounds UIScreen::GetLayoutBounds(UIContext &dc) const { - return dc.GetLayoutBounds(LayoutMode(), UseImmersiveMode()); + return dc.GetLayoutBounds(LayoutMode(), UseImmersiveMode(), ReserveHiddenNavBar()); } // On TVs and big monitors, controls out at the edges are hard to see and far apart. Grows with the @@ -151,7 +151,7 @@ static float LargeScreenInset() { Bounds UIScreen::GetContentBounds(const UIContext &dc) const { const UI::Margins margins = RootMargins(); - Bounds bounds = dc.GetLayoutBounds(LayoutMode(), UseImmersiveMode()); + Bounds bounds = dc.GetLayoutBounds(LayoutMode(), UseImmersiveMode(), ReserveHiddenNavBar()); bounds.x += margins.left; bounds.y += margins.top; bounds.w -= margins.horiz(); diff --git a/Common/UI/UIScreen.h b/Common/UI/UIScreen.h index 2792bb567c..f0085c21de 100644 --- a/Common/UI/UIScreen.h +++ b/Common/UI/UIScreen.h @@ -69,6 +69,8 @@ protected: bool IsOnTop() const; virtual ViewLayoutMode LayoutMode() const { return ViewLayoutMode::ApplyInsets; } virtual bool UseImmersiveMode() const { return false; } + // For screens with buttons near the edges: a swipe can bring back a hidden navigation bar over them. + virtual bool ReserveHiddenNavBar() const { return false; } UI::ViewGroup *root_ = nullptr; Vec3 translation_ = Vec3(0.0f); diff --git a/UI/PauseScreen.h b/UI/PauseScreen.h index 1a02e968b1..c11e3ebc03 100644 --- a/UI/PauseScreen.h +++ b/UI/PauseScreen.h @@ -51,6 +51,7 @@ protected: ViewLayoutMode LayoutMode() const override { return ViewLayoutMode::ApplyInsets; } + bool ReserveHiddenNavBar() const override { return true; } void OnVKey(VirtKey virtualKeyCode, bool down) override; private: diff --git a/android/jni/app-android.cpp b/android/jni/app-android.cpp index 8df90b0f21..c05fa5ffb6 100644 --- a/android/jni/app-android.cpp +++ b/android/jni/app-android.cpp @@ -173,6 +173,10 @@ static float g_safeInsetLeft = 0.0; static float g_safeInsetRight = 0.0; static float g_safeInsetTop = 0.0; static float g_safeInsetBottom = 0.0; +static float g_navBarInsetLeft = 0.0; +static float g_navBarInsetRight = 0.0; +static float g_navBarInsetTop = 0.0; +static float g_navBarInsetBottom = 0.0; static bool g_hasCameraCutout = false; static bool g_hasAccelerometer = false; static bool g_hasGyro = false; @@ -400,6 +404,14 @@ float System_GetPropertyFloat(SystemProperty prop) { return g_safeInsetTop * display_scale_y * g_display.dpi_scale_y; case SYSPROP_DISPLAY_SAFE_INSET_BOTTOM: return g_safeInsetBottom * display_scale_y * g_display.dpi_scale_y; + case SYSPROP_DISPLAY_NAVBAR_INSET_LEFT: + return g_navBarInsetLeft * display_scale_x * g_display.dpi_scale_x; + case SYSPROP_DISPLAY_NAVBAR_INSET_RIGHT: + return g_navBarInsetRight * display_scale_x * g_display.dpi_scale_x; + case SYSPROP_DISPLAY_NAVBAR_INSET_TOP: + return g_navBarInsetTop * display_scale_y * g_display.dpi_scale_y; + case SYSPROP_DISPLAY_NAVBAR_INSET_BOTTOM: + return g_navBarInsetBottom * display_scale_y * g_display.dpi_scale_y; default: return -1; } @@ -1466,13 +1478,17 @@ extern "C" void JNICALL Java_org_ppsspp_ppsspp_NativeApp_sendMessageFromJava(JNI } else if (msg == "safe_insets") { // INFO_LOG(Log::System, "Got insets: %s", prm.c_str()); // We don't bother with supporting exact rectangular regions. Safe insets are good enough. - int left, right, top, bottom, cutout; - if (5 == sscanf(prm.c_str(), "%d:%d:%d:%d:%d", &left, &right, &top, &bottom, &cutout)) { + int left, right, top, bottom, cutout, navLeft, navRight, navTop, navBottom; + if (9 == sscanf(prm.c_str(), "%d:%d:%d:%d:%d:%d:%d:%d:%d", &left, &right, &top, &bottom, &cutout, &navLeft, &navRight, &navTop, &navBottom)) { g_safeInsetLeft = (float)left; g_safeInsetRight = (float)right; g_safeInsetTop = (float)top; g_safeInsetBottom = (float)bottom; g_hasCameraCutout = cutout != 0; + g_navBarInsetLeft = (float)navLeft; + g_navBarInsetRight = (float)navRight; + g_navBarInsetTop = (float)navTop; + g_navBarInsetBottom = (float)navBottom; } } else if (msg == "sensors") { int accelerometer, gyro; diff --git a/android/src/org/ppsspp/ppsspp/PpssppActivity.java b/android/src/org/ppsspp/ppsspp/PpssppActivity.java index 7229ceffba..682bddf66d 100644 --- a/android/src/org/ppsspp/ppsspp/PpssppActivity.java +++ b/android/src/org/ppsspp/ppsspp/PpssppActivity.java @@ -975,7 +975,10 @@ public class PpssppActivity extends AppCompatActivity implements SensorEventList int right = insets.right; int top = insets.top; int bottom = insets.bottom; - NativeApp.sendMessageFromJava("safe_insets", left + ":" + right + ":" + top + ":" + bottom + ":" + (hasCameraCutout ? 1 : 0)); + // Where the navigation bar is when shown, which it is for a while after a swipe from the edge even when hidden. + Insets navBar = insetCompat.getInsetsIgnoringVisibility(WindowInsetsCompat.Type.navigationBars()); + NativeApp.sendMessageFromJava("safe_insets", left + ":" + right + ":" + top + ":" + bottom + ":" + (hasCameraCutout ? 1 : 0) + + ":" + navBar.left + ":" + navBar.right + ":" + navBar.top + ":" + navBar.bottom); } public void notifySurface(Surface surface) { From 1ef159f3e4aa62c4c9809d19bc13fefc36e11435 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Henrik=20Rydg=C3=A5rd?= Date: Mon, 9 Nov 2026 11:02:37 +0100 Subject: [PATCH 2/2] Prettify some popups. --- Common/UI/PopupScreens.cpp | 1 - Common/UI/ViewGroup.cpp | 1 + UI/AdhocServerScreen.cpp | 38 ++++++++++++++++---------------------- UI/AdhocServerScreen.h | 1 - 4 files changed, 17 insertions(+), 24 deletions(-) diff --git a/Common/UI/PopupScreens.cpp b/Common/UI/PopupScreens.cpp index 4db4ab4114..a30a27541b 100644 --- a/Common/UI/PopupScreens.cpp +++ b/Common/UI/PopupScreens.cpp @@ -238,7 +238,6 @@ void PopupScreen::CreateViews() { box_->Add(buttonRow); } - box_->SetRoundCorners(RoundCorners::All); // Rounds the top of the header and the bottom of the last view, to match the box. box_->SetRoundCorners(RoundCorners::All); } diff --git a/Common/UI/ViewGroup.cpp b/Common/UI/ViewGroup.cpp index 2ca3e01fdf..63c385222f 100644 --- a/Common/UI/ViewGroup.cpp +++ b/Common/UI/ViewGroup.cpp @@ -853,6 +853,7 @@ void LinearLayout::Layout() { // Applies rounding to the corners of the child views, based on their position in the layout and the orientation. Masked by the outerCorners. // Ignores invisible views. void LinearLayout::SetRoundCorners(RoundCorners outerCorners) { + roundCorners_ = outerCorners; // Easy case first: There's spacing, just apply round corners to all subviews. if (spacing_ > 0.0f) { for (View *view : views_) { diff --git a/UI/AdhocServerScreen.cpp b/UI/AdhocServerScreen.cpp index 30c7b2368a..6695308ba0 100644 --- a/UI/AdhocServerScreen.cpp +++ b/UI/AdhocServerScreen.cpp @@ -217,12 +217,12 @@ private: }; AdhocServerCompactInfo::AdhocServerCompactInfo(const AdhocServerListEntry &entry, UI::LayoutParams *layoutParams) - : UI::LinearLayout(ORIENT_HORIZONTAL, new UI::LinearLayoutParams(UI::FILL_PARENT, UI::WRAP_CONTENT, UI::Margins(5.0f, 0.0f))), entry_(entry) { + : UI::LinearLayout(ORIENT_HORIZONTAL, new UI::LinearLayoutParams(UI::FILL_PARENT, UI::WRAP_CONTENT)), entry_(entry) { using namespace UI; SetSpacing(5.0f); - LinearLayout *lines = Add(new LinearLayout(ORIENT_VERTICAL, new LinearLayoutParams(Margins(5, 5)))); + LinearLayout *lines = Add(new LinearLayout(ORIENT_VERTICAL)); lines->SetSpacing(0.0f); TextView *name = lines->Add(new TextView(entry.name)); @@ -234,10 +234,10 @@ AdhocServerCompactInfo::AdhocServerCompactInfo(const AdhocServerListEntry &entry lines->Add(new TextView(secondLine))->SetTextSize(TextSize::Small)->SetWordWrap(); - Add(new Spacer(0.0f, new LinearLayoutParams(1.0f, Margins(0.0f, 5.0f)))); + Add(new Spacer(0.0f, new LinearLayoutParams(1.0f))); if (entry.mode == AdhocDataMode::AemuPostoffice) { - TextView *relay = Add(new TextView(n->T("Relay"), new LinearLayoutParams(WRAP_CONTENT, WRAP_CONTENT, Gravity::G_VCENTER, Margins(10.0)))); + TextView *relay = Add(new TextView(n->T("Relay"), new LinearLayoutParams(WRAP_CONTENT, WRAP_CONTENT, Gravity::G_VCENTER, Margins(0.0)))); } Add(new Choice(ImageID("I_FILE_COPY"), new LinearLayoutParams(WRAP_CONTENT, WRAP_CONTENT)))->OnClick.Add([host = entry_.host](UI::EventParams &) { @@ -245,12 +245,6 @@ AdhocServerCompactInfo::AdhocServerCompactInfo(const AdhocServerListEntry &entry }); } -void AdhocServerCompactInfo::Draw(UIContext &dc) { - UI::LinearLayout::Draw(dc); - // Underline - dc.Draw()->DrawImageCenterTexel(dc.GetTheme().whiteImage, bounds_.x, bounds_.y2() - 2, bounds_.x2(), bounds_.y2(), dc.GetTheme().popupTitleStyle.fgColor); -} - static UI::View *CreateInfoItemWithButton(std::string_view text, ImageID buttonImage, std::function onClick) { using namespace UI; LinearLayout *line = new LinearLayout(ORIENT_HORIZONTAL, new LinearLayoutParams(FILL_PARENT, WRAP_CONTENT, Margins(12, 0))); @@ -372,22 +366,18 @@ void AdhocServerInfoScreen::CreatePopupContents(UI::ViewGroup *parent) { auto di = GetI18NCategory(I18NCat::DIALOG); auto ni = GetI18NCategory(I18NCat::NETWORKING); - Margins contentMargins(12, 0); - - parent->Add(new AdhocServerCompactInfo(entry_, new LinearLayoutParams(FILL_PARENT, WRAP_CONTENT, contentMargins))); - parent->Add(new Spacer(5.0f)); + parent->Add(new AdhocServerCompactInfo(entry_, new LinearLayoutParams(FILL_PARENT, WRAP_CONTENT))); ScrollView *scroll = new ScrollView(ORIENT_VERTICAL, new LinearLayoutParams(FILL_PARENT, WRAP_CONTENT, 1.0f)); LinearLayout *content = new LinearLayout(ORIENT_VERTICAL); - content->SetSpacing(6.0f); + content->SetSpacing(8.0f); if (!entry_.ip.empty()) { content->Add(new InfoItem(entry_.ip, "")); } - TextView *desc = content->Add(new TextView(entry_.description, new LinearLayoutParams(FILL_PARENT, WRAP_CONTENT, contentMargins))); - desc->SetTextSize(TextSize::Small); - desc->SetWordWrap(); + InfoItem *desc = content->Add(new InfoItem(entry_.description, "", new LinearLayoutParams(FILL_PARENT, WRAP_CONTENT))); + desc->SetRoundCorners(); - LinearLayout *buttonStrip = content->Add(new LinearLayout(ORIENT_HORIZONTAL, new LinearLayoutParams(FILL_PARENT, WRAP_CONTENT, contentMargins))); + LinearLayout *buttonStrip = content->Add(new LinearLayout(ORIENT_HORIZONTAL, new LinearLayoutParams(FILL_PARENT, WRAP_CONTENT))); buttonStrip->SetSpacing(8); if (!entry_.web.empty() || !entry_.discord.empty()) { if (!entry_.web.empty()) { @@ -412,6 +402,8 @@ void AdhocServerInfoScreen::CreatePopupContents(UI::ViewGroup *parent) { CreateAdhocServerGameList(content, games_, statusRequest_.get() ? true : false); } + buttonStrip->SetRoundCorners(); + scroll->Add(content); parent->Add(scroll); } @@ -500,16 +492,17 @@ private: }; AdhocServerRow::AdhocServerRow(std::string *editValue, const AdhocServerListEntry &entry, bool showDeleteButton, ScreenManager *screenManager, UI::LayoutParams *layoutParams) - : UI::LinearLayout(ORIENT_HORIZONTAL, new UI::LinearLayoutParams(UI::FILL_PARENT, UI::WRAP_CONTENT, UI::Margins(5.0f, 0.0f))), value_(editValue), entry_(entry) { + : UI::LinearLayout(ORIENT_HORIZONTAL, new UI::LinearLayoutParams(UI::FILL_PARENT, UI::WRAP_CONTENT)), value_(editValue), entry_(entry) { using namespace UI; SetSpacing(5.0f); + SetPadding(Margins(10.0f, 2.0f, 0.0f, 2.0f)); // Show as radio button to make it really clear that selection actually is the choice. int number = 0; Add(new ImageView([editValue, host = entry.host]() { return host == *editValue ? ImageID("I_RADIO_SELECTED") : ImageID("I_RADIO_EMPTY"); }, - new LinearLayoutParams(WRAP_CONTENT, WRAP_CONTENT, Gravity::G_VCENTER, Margins(5, 0, 0, 0)))); + new LinearLayoutParams(WRAP_CONTENT, WRAP_CONTENT, Gravity::G_VCENTER, Margins(0.0f, 0.0f, 6.0f, 0.0f)))); - LinearLayout *lines = Add(new LinearLayout(ORIENT_VERTICAL, new LinearLayoutParams(Margins(5, 5)))); + LinearLayout *lines = Add(new LinearLayout(ORIENT_VERTICAL, new LinearLayoutParams(Margins(0.0f, 5)))); lines->SetSpacing(0.0f); ClickableTextView *name = lines->Add(new ClickableTextView(entry.name)); name->SetFocusable(true); @@ -547,6 +540,7 @@ AdhocServerRow::AdhocServerRow(std::string *editValue, const AdhocServerListEntr e.v = this; screenManager->push(new AdhocServerInfoScreen(copy)); }); + infoButton->SetRoundCorners(); } } diff --git a/UI/AdhocServerScreen.h b/UI/AdhocServerScreen.h index 572bce44c9..c7af2f2999 100644 --- a/UI/AdhocServerScreen.h +++ b/UI/AdhocServerScreen.h @@ -33,7 +33,6 @@ struct AdhocGame { class AdhocServerCompactInfo : public UI::LinearLayout { public: AdhocServerCompactInfo(const AdhocServerListEntry &entry, UI::LayoutParams *layoutParams = nullptr); - void Draw(UIContext &dc) override; void GetContentDimensions(const UIContext &dc, float &w, float &h) const override { w = 500; h = 100; }