From 1085fcc820e5317060a594f8171cb62ce2c0c8c5 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Henrik=20Rydg=C3=A5rd?= Date: Thu, 5 Nov 2026 18:08:08 +0100 Subject: [PATCH 01/14] Fix icon size problem with directory, add icon for Restore Default button on speed hacks screen --- UI/GameButton.cpp | 10 ++++++++-- UI/SettingsPage.cpp | 2 +- 2 files changed, 9 insertions(+), 3 deletions(-) diff --git a/UI/GameButton.cpp b/UI/GameButton.cpp index 56a0675833..b85be43515 100644 --- a/UI/GameButton.cpp +++ b/UI/GameButton.cpp @@ -354,8 +354,14 @@ void GameButton::Draw(UIContext &dc) { if (imageIcon.isValid()) { const float textX = ListTextX(viewMode_); if (viewMode_ == GameBrowserViewMode::LARGE_LIST) { - const float scale = g_Config.fGameGridScale; - dc.Draw()->DrawImage(imageIcon, bounds.x + LARGE_LIST_LEFT_PAD + LARGE_LIST_ICON_W * 0.5f * scale, bounds.centerY(), 0.88f * scale, style.fgColor, ALIGN_CENTER); + float scale = g_Config.fGameGridScale; + if (type_ == GameButtonType::Dir) { + // Dirs are fixed height + scale = 1.0f; + } else { + scale = g_Config.fGameGridScale * 0.88f; + } + dc.Draw()->DrawImage(imageIcon, bounds.x + LARGE_LIST_LEFT_PAD + LARGE_LIST_ICON_W * g_Config.fGameGridScale * 0.5f, bounds.centerY(), scale, style.fgColor, ALIGN_CENTER); } else { dc.Draw()->DrawImage(imageIcon, bounds.x + 72, bounds.centerY(), 0.88f, style.fgColor, ALIGN_CENTER); } diff --git a/UI/SettingsPage.cpp b/UI/SettingsPage.cpp index 1d0969e0fa..af1c4d90a3 100644 --- a/UI/SettingsPage.cpp +++ b/UI/SettingsPage.cpp @@ -219,7 +219,7 @@ SettingItem &SettingsPage::Link(SettingText text, I18NCat cat, std::function Date: Thu, 5 Nov 2026 18:20:33 +0100 Subject: [PATCH 02/14] UI: Let RadioButton show a hint, like CheckBox RadioButton is now a ClickableItem, so it fills its parent's width by default and draws its hint text below the title, lined up with the text after the radio circle. Co-Authored-By: Claude Opus 5.5 --- Common/UI/View.cpp | 27 ++++++++++++++++--------- Common/UI/View.h | 49 +++++++++++++++++++++++++--------------------- 2 files changed, 45 insertions(+), 31 deletions(-) diff --git a/Common/UI/View.cpp b/Common/UI/View.cpp index de6abe7232..c1d81ad9bb 100644 --- a/Common/UI/View.cpp +++ b/Common/UI/View.cpp @@ -470,7 +470,7 @@ void ClickableItem::Measure(const UIContext &dc, MeasureSpec horiz, MeasureSpec if (hintText_.empty()) { return; } - float availWidth = measuredWidth_ - HINT_PADDING_X * 2; + float availWidth = measuredWidth_ - HintIndent() - HINT_PADDING_X; if (availWidth <= 0.0f) { availWidth = MAX_ITEM_SIZE; } @@ -487,11 +487,16 @@ void ClickableItem::DrawHint(UIContext &dc, const Style &style) const { dc.SetFontStyle(dc.GetTheme().uiFontTiny); dc.SetFontScale(1.0f, 1.0f); const float textHeight = hintHeight_ - HINT_PADDING_BOTTOM + HINT_TITLE_OVERLAP; - Bounds hintBounds(bounds_.x + HINT_PADDING_X, bounds_.y2() - HINT_PADDING_BOTTOM - textHeight, bounds_.w - HINT_PADDING_X * 2, textHeight); + const float indent = HintIndent(); + Bounds hintBounds(bounds_.x + indent, bounds_.y2() - HINT_PADDING_BOTTOM - textHeight, bounds_.w - indent - HINT_PADDING_X, textHeight); dc.DrawTextRect(hintText_, hintBounds, style.fgColor, ALIGN_TOPLEFT | FLAG_WRAP_TEXT); dc.SetFontStyle(dc.GetTheme().uiFont); } +float ClickableItem::HintIndent() const { + return HINT_PADDING_X; +} + void Choice::ClickInternal() { ClickableItem::ClickInternal(); UI::PlayUISound(UI::UISound::CONFIRM); @@ -1097,11 +1102,13 @@ void RadioButton::Draw(UIContext &dc) { DrawBG(dc, style, dc.GetTheme().choiceRoundRadius); + const Bounds bounds = ContentBounds(); + dc.Flush(); dc.BeginNoTex(); - dc.Draw()->Circle(bounds_.x + paddingW_ + radioRadius_, bounds_.centerY(), radioRadius_, 2.5f, 36, 0, style.fgColor, 1.0f); + dc.Draw()->Circle(bounds.x + paddingW_ + radioRadius_, bounds.centerY(), radioRadius_, 2.5f, 36, 0, style.fgColor, 1.0f); if (checked) { - dc.Draw()->FillCircle(bounds_.x + paddingW_ + radioRadius_, bounds_.centerY(), radioInnerRadius_, 36, style.fgColor); + dc.Draw()->FillCircle(bounds.x + paddingW_ + radioRadius_, bounds.centerY(), radioInnerRadius_, 36, style.fgColor); } dc.Flush(); dc.Begin(); @@ -1109,20 +1116,22 @@ void RadioButton::Draw(UIContext &dc) { float tw, th; dc.MeasureText(dc.GetTheme().uiFont, 1.0f, 1.0f, text_, &tw, &th); - if (tw > bounds_.w) { - dc.PushScissor(bounds_); + if (tw > bounds.w) { + dc.PushScissor(bounds); } dc.SetFontStyle(dc.GetTheme().uiFont); if (!text_.empty()) { - float textX = bounds_.x + paddingW_ * 2.0f + radioRadius_ * 2.0f; - dc.DrawText(text_, textX, bounds_.centerY(), style.fgColor, ALIGN_LEFT | ALIGN_VCENTER); + float textX = bounds.x + paddingW_ * 2.0f + radioRadius_ * 2.0f; + dc.DrawText(text_, textX, bounds.centerY(), style.fgColor, ALIGN_LEFT | ALIGN_VCENTER); } - if (tw > bounds_.w) { + if (tw > bounds.w) { dc.PopScissor(); } + + DrawHint(dc, style); } ImageView::ImageView(ImageID atlasImage, const std::string &text, LayoutParams *layoutParams) diff --git a/Common/UI/View.h b/Common/UI/View.h index 9a2911f898..81a7afd601 100644 --- a/Common/UI/View.h +++ b/Common/UI/View.h @@ -564,7 +564,6 @@ public: bool CanBeFocused() const override { return false; } }; - // All these light up their background when touched, or have focus. class Clickable : public View { public: @@ -639,26 +638,6 @@ private: bool ignoreText_ = false; }; -class RadioButton : public Clickable { -public: - RadioButton(int *value, int thisButtonValue, std::string_view text, LayoutParams *layoutParams = 0) - : Clickable(layoutParams), value_(value), thisButtonValue_(thisButtonValue), text_(text) {} - void Draw(UIContext &dc) override; - void GetContentDimensions(const UIContext &dc, float &w, float &h) const override; - std::string DescribeText() const override; - -private: - void ClickInternal() override; - int *value_; - int thisButtonValue_; - std::string text_; - const float paddingW_ = 8; - const float paddingH_ = 4; - - const float radioRadius_ = 16.0f; - const float radioInnerRadius_ = 8.0f; -}; - class Slider : public Clickable { public: Slider(int *value, int minValue, int maxValue, int step = 1, LayoutParams *layoutParams = nullptr) @@ -768,7 +747,7 @@ public: void Draw(UIContext &dc) override; // Small explanatory text under the item, part of it (highlighted and clicked along with it). - // Drawn by Choice and CheckBox. + // Drawn by Choice and CheckBox and RadioButton. void SetHintText(std::string_view text) { hintText_ = text; } @@ -784,6 +763,9 @@ protected: return b; } void DrawHint(UIContext &dc, const Style &style) const; + // Where the hint starts, from the item's left edge. Items whose text doesn't start at the + // default padding override this, to line the hint up with it. + virtual float HintIndent() const; std::string hintText_; float hintHeight_ = 0.0f; @@ -1013,6 +995,29 @@ protected: ImageID imageID_; }; +class RadioButton : public ClickableItem { +public: + RadioButton(int *value, int thisButtonValue, std::string_view text, LayoutParams *layoutParams = nullptr) + : ClickableItem(layoutParams), value_(value), thisButtonValue_(thisButtonValue), text_(text) {} + void Draw(UIContext &dc) override; + void GetContentDimensions(const UIContext &dc, float &w, float &h) const override; + std::string DescribeText() const override; + +protected: + float HintIndent() const override { return paddingW_ * 2.0f + radioRadius_ * 2.0f; } + +private: + void ClickInternal() override; + int *value_; + int thisButtonValue_; + std::string text_; + const float paddingW_ = 8; + const float paddingH_ = 4; + + const float radioRadius_ = 16.0f; + const float radioInnerRadius_ = 8.0f; +}; + class CollapsibleHeader : public CheckBox { public: CollapsibleHeader(bool *open, std::string_view text, LayoutParams *layoutParams = nullptr); From 70e0782da8972af785710b3b7bf41fb4a4e78798 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Henrik=20Rydg=C3=A5rd?= Date: Thu, 5 Nov 2026 18:23:42 +0100 Subject: [PATCH 03/14] UI: Draw RadioButton with the I_RADIO_* atlas images Co-Authored-By: Claude Opus 5.5 --- Common/UI/View.cpp | 10 ++-------- Common/UI/View.h | 2 +- 2 files changed, 3 insertions(+), 9 deletions(-) diff --git a/Common/UI/View.cpp b/Common/UI/View.cpp index c1d81ad9bb..637709a296 100644 --- a/Common/UI/View.cpp +++ b/Common/UI/View.cpp @@ -1104,14 +1104,8 @@ void RadioButton::Draw(UIContext &dc) { const Bounds bounds = ContentBounds(); - dc.Flush(); - dc.BeginNoTex(); - dc.Draw()->Circle(bounds.x + paddingW_ + radioRadius_, bounds.centerY(), radioRadius_, 2.5f, 36, 0, style.fgColor, 1.0f); - if (checked) { - dc.Draw()->FillCircle(bounds.x + paddingW_ + radioRadius_, bounds.centerY(), radioInnerRadius_, 36, style.fgColor); - } - dc.Flush(); - dc.Begin(); + const ImageID image(checked ? "I_RADIO_SELECTED" : "I_RADIO_EMPTY"); + dc.Draw()->DrawImage(image, bounds.x + paddingW_ + radioRadius_, bounds.centerY(), 1.0f, style.fgColor, ALIGN_CENTER); float tw, th; dc.MeasureText(dc.GetTheme().uiFont, 1.0f, 1.0f, text_, &tw, &th); diff --git a/Common/UI/View.h b/Common/UI/View.h index 81a7afd601..d3322280aa 100644 --- a/Common/UI/View.h +++ b/Common/UI/View.h @@ -1014,8 +1014,8 @@ private: const float paddingW_ = 8; const float paddingH_ = 4; + // Half the size of the I_RADIO_* images. const float radioRadius_ = 16.0f; - const float radioInnerRadius_ = 8.0f; }; class CollapsibleHeader : public CheckBox { From c936b544cfa9739821c0c7084e08b788afbae4b8 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Henrik=20Rydg=C3=A5rd?= Date: Thu, 5 Nov 2026 18:28:39 +0100 Subject: [PATCH 04/14] rcheevos: Bump to top of tree to get security fixes --- ext/rcheevos | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/ext/rcheevos b/ext/rcheevos index 1433173220..eea6461f17 160000 --- a/ext/rcheevos +++ b/ext/rcheevos @@ -1 +1 @@ -Subproject commit 1433173220a7eaede6a9ed7a18e94117be1821e0 +Subproject commit eea6461f172d37d36b0d56568f13a505e0a9fd86 From 28eddf4f6b543aeab529fba6f026dd62d95528f7 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Henrik=20Rydg=C3=A5rd?= Date: Thu, 5 Nov 2026 18:35:42 +0100 Subject: [PATCH 05/14] rcheevos: Update the build files for the new source layout Upstream moved the sources into api/, client/, runtime/ and util/ directories, renamed most of them, and split rc_client.c into several files. The lists now match upstream's src/Makefile.common, which also drops rc_validate.c (nothing calls it). Also follow the API rename of core/unofficial achievements to promoted/unpromoted. Co-Authored-By: Claude Opus 5.5 --- Core/RetroAchievements.cpp | 12 +- UI/RetroAchievementScreens.cpp | 6 +- UWP/rcheevos_UWP/rcheevos_UWP.vcxproj | 99 ++++-- UWP/rcheevos_UWP/rcheevos_UWP.vcxproj.filters | 316 ++++++++++++------ android/jni/Android.mk | 56 ++-- ext/rcheevos-build/CMakeLists.txt | 102 ++++-- ext/rcheevos-build/rcheevos.vcxproj | 102 ++++-- ext/rcheevos-build/rcheevos.vcxproj.filters | 259 +++++++++----- libretro/Makefile.common | 56 ++-- 9 files changed, 659 insertions(+), 349 deletions(-) diff --git a/Core/RetroAchievements.cpp b/Core/RetroAchievements.cpp index 14e6b3b324..076dee6b58 100644 --- a/Core/RetroAchievements.cpp +++ b/Core/RetroAchievements.cpp @@ -229,7 +229,7 @@ bool UnofficialEnabled() { if (!g_rcClient) { return false; } - return rc_client_get_unofficial_enabled(g_rcClient); + return rc_client_get_unpromoted_enabled(g_rcClient); } bool HardcoreModeActive() { @@ -744,7 +744,7 @@ void Initialize() { // Set initial settings properly. Hardcore mode is checked by RAIntegration. rc_client_set_hardcore_enabled(g_rcClient, g_Config.bAchievementsHardcoreMode ? 1 : 0); rc_client_set_encore_mode_enabled(g_rcClient, g_Config.bAchievementsEncoreMode ? 1 : 0); - rc_client_set_unofficial_enabled(g_rcClient, g_Config.bAchievementsUnofficial ? 1 : 0); + rc_client_set_unpromoted_enabled(g_rcClient, g_Config.bAchievementsUnofficial ? 1 : 0); #ifdef RC_CLIENT_SUPPORTS_RAINTEGRATION if (!g_Config.bAchievementsEnableRAIntegration) { @@ -1096,12 +1096,12 @@ std::string GetGameAchievementSummary(uint32_t subsetId) { } std::string summaryString; - if (summary.num_core_achievements + summary.num_unofficial_achievements == 0) { + if (summary.num_promoted_achievements + summary.num_unpromoted_achievements == 0) { summaryString = ac->T("This game has no achievements"); } else { summaryString = ApplySafeSubstitutions(ac->T("Earned", "You have unlocked %1 of %2 achievements, earning %3 of %4 points"), - summary.num_unlocked_achievements, summary.num_core_achievements + summary.num_unofficial_achievements, - summary.points_unlocked, summary.points_core); + summary.num_unlocked_achievements, summary.num_promoted_achievements + summary.num_unpromoted_achievements, + summary.points_unlocked, summary.points_available); if (HardcoreModeActive()) { summaryString.append("\n"); summaryString.append(ac->T("Hardcore Mode")); @@ -1248,7 +1248,7 @@ void SetGame(const Path &path, IdentifiedFileType fileType, FileLoader *fileLoad // Apply pre-load settings. rc_client_set_hardcore_enabled(g_rcClient, g_Config.bAchievementsHardcoreMode ? 1 : 0); rc_client_set_encore_mode_enabled(g_rcClient, g_Config.bAchievementsEncoreMode ? 1 : 0); - rc_client_set_unofficial_enabled(g_rcClient, g_Config.bAchievementsUnofficial ? 1 : 0); + rc_client_set_unpromoted_enabled(g_rcClient, g_Config.bAchievementsUnofficial ? 1 : 0); rc_client_begin_load_game(g_rcClient, s_game_hash.c_str(), &identify_and_load_callback, nullptr); } diff --git a/UI/RetroAchievementScreens.cpp b/UI/RetroAchievementScreens.cpp index 07a7ab57b7..2b3f24e9e4 100644 --- a/UI/RetroAchievementScreens.cpp +++ b/UI/RetroAchievementScreens.cpp @@ -79,7 +79,7 @@ static const char *AchievementBucketTitle(int bucketType) { case RC_CLIENT_ACHIEVEMENT_BUCKET_LOCKED: return "Locked"; case RC_CLIENT_ACHIEVEMENT_BUCKET_UNLOCKED: return "Unlocked"; case RC_CLIENT_ACHIEVEMENT_BUCKET_UNSUPPORTED: return "Unsupported"; - case RC_CLIENT_ACHIEVEMENT_BUCKET_UNOFFICIAL: return "Unofficial"; + case RC_CLIENT_ACHIEVEMENT_BUCKET_UNPROMOTED: return "Unofficial"; case RC_CLIENT_ACHIEVEMENT_BUCKET_RECENTLY_UNLOCKED: return "Recently unlocked"; case RC_CLIENT_ACHIEVEMENT_BUCKET_ACTIVE_CHALLENGE: return "Achievements with active challenges"; case RC_CLIENT_ACHIEVEMENT_BUCKET_ALMOST_THERE: return "Almost completed"; @@ -88,9 +88,9 @@ static const char *AchievementBucketTitle(int bucketType) { } static uint32_t GetListFilter() { - int filter = RC_CLIENT_ACHIEVEMENT_CATEGORY_CORE; + int filter = RC_CLIENT_ACHIEVEMENT_CATEGORY_PROMOTED; if (Achievements::UnofficialEnabled()) { - filter = RC_CLIENT_ACHIEVEMENT_CATEGORY_CORE_AND_UNOFFICIAL; + filter = RC_CLIENT_ACHIEVEMENT_CATEGORY_PROMOTED_AND_UNPROMOTED; } return filter; } diff --git a/UWP/rcheevos_UWP/rcheevos_UWP.vcxproj b/UWP/rcheevos_UWP/rcheevos_UWP.vcxproj index b448fbd889..accd4b2f53 100644 --- a/UWP/rcheevos_UWP/rcheevos_UWP.vcxproj +++ b/UWP/rcheevos_UWP/rcheevos_UWP.vcxproj @@ -19,52 +19,85 @@ - + + + - - - - - - - - - + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + - - - - - - - - - - - - - - - - - - - - - - - + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + {4c9d52d0-310a-4347-8991-e3788cb22169} diff --git a/UWP/rcheevos_UWP/rcheevos_UWP.vcxproj.filters b/UWP/rcheevos_UWP/rcheevos_UWP.vcxproj.filters index 28d7abc2fe..0fc4b6c366 100644 --- a/UWP/rcheevos_UWP/rcheevos_UWP.vcxproj.filters +++ b/UWP/rcheevos_UWP/rcheevos_UWP.vcxproj.filters @@ -1,5 +1,123 @@  + + + {5dd27146-a8f0-4030-b154-0e24070261ed} + + + {6ef1a114-9e1e-4b7a-8f55-31b7ec4162a5} + + + {eb1c470f-99ef-4a57-8a15-91f375d809f6} + + + {54a6b682-2623-4844-b94c-bda70a4e2c5d} + + + {ef13705b-0bc4-4f06-8daa-7f4a7234a715} + + + + + api + + + api + + + api + + + api + + + api + + + client + + + client + + + client + + + client + + + client + + + client + + + client + + + client + + + client + + + client + + + runtime + + + runtime + + + runtime + + + runtime + + + runtime + + + runtime + + + runtime + + + runtime + + + runtime + + + runtime + + + runtime + + + runtime + + + runtime + + + runtime + + + util + + + util + + + util + + + util + + include @@ -16,138 +134,122 @@ include + + include + + + include + + + include + include include + + include + include include - - include - - + include include - - rapi + + api - - rcheevos + + client - - rcheevos + + client - - rcheevos + + client - - rcheevos + + client - - include + + client - - rhash + + client - - rcheevos + + client - - include + + client + + + client + + + runtime + + + runtime + + + runtime + + + runtime + + + runtime + + + runtime + + + runtime + + + runtime + + + runtime + + + runtime + + + runtime + + + runtime + + + runtime + + + runtime + + + runtime + + + runtime + + + util + + + util + + + util - - - - {918cc87d-a38b-4e00-be13-0256398e1eec} - - - {89858679-3cc1-4b8b-b2f7-7f418d9335e9} - - - {e161d2c8-a15a-499d-96d9-fc29fb4ff78d} - - - {4e62f4d5-be01-4047-b0d6-3c0c171ff486} - - - - - rapi - - - rapi - - - rapi - - - rapi - - - rapi - - - rcheevos - - - rcheevos - - - rcheevos - - - rcheevos - - - rcheevos - - - rcheevos - - - rcheevos - - - rcheevos - - - rcheevos - - - rcheevos - - - rcheevos - - - rcheevos - - - rcheevos - - - rcheevos - - - rcheevos - - - rhash - - - rcheevos - - - rcheevos - \ No newline at end of file diff --git a/android/jni/Android.mk b/android/jni/Android.mk index 89e4529976..f4a78d738f 100644 --- a/android/jni/Android.mk +++ b/android/jni/Android.mk @@ -250,29 +250,39 @@ AT3_STANDALONE_FILES := \ ${SRC}/ext/at3_standalone/mem.cpp RCHEEVOS_FILES := \ - ${SRC}/ext/rcheevos/src/rapi/rc_api_common.c \ - ${SRC}/ext/rcheevos/src/rapi/rc_api_editor.c \ - ${SRC}/ext/rcheevos/src/rapi/rc_api_info.c \ - ${SRC}/ext/rcheevos/src/rapi/rc_api_runtime.c \ - ${SRC}/ext/rcheevos/src/rapi/rc_api_user.c \ - ${SRC}/ext/rcheevos/src/rcheevos/alloc.c \ - ${SRC}/ext/rcheevos/src/rcheevos/condition.c \ - ${SRC}/ext/rcheevos/src/rcheevos/condset.c \ - ${SRC}/ext/rcheevos/src/rcheevos/consoleinfo.c \ - ${SRC}/ext/rcheevos/src/rcheevos/format.c \ - ${SRC}/ext/rcheevos/src/rcheevos/lboard.c \ - ${SRC}/ext/rcheevos/src/rcheevos/memref.c \ - ${SRC}/ext/rcheevos/src/rcheevos/operand.c \ - ${SRC}/ext/rcheevos/src/rc_client.c \ - ${SRC}/ext/rcheevos/src/rc_util.c \ - ${SRC}/ext/rcheevos/src/rc_compat.c \ - ${SRC}/ext/rcheevos/src/rcheevos/rc_validate.c \ - ${SRC}/ext/rcheevos/src/rcheevos/richpresence.c \ - ${SRC}/ext/rcheevos/src/rcheevos/runtime.c \ - ${SRC}/ext/rcheevos/src/rcheevos/runtime_progress.c \ - ${SRC}/ext/rcheevos/src/rcheevos/trigger.c \ - ${SRC}/ext/rcheevos/src/rcheevos/value.c \ - ${SRC}/ext/rcheevos/src/rhash/md5.c + ${SRC}/ext/rcheevos/src/api/rc_api_common.c \ + ${SRC}/ext/rcheevos/src/api/rc_api_editor.c \ + ${SRC}/ext/rcheevos/src/api/rc_api_info.c \ + ${SRC}/ext/rcheevos/src/api/rc_api_runtime.c \ + ${SRC}/ext/rcheevos/src/api/rc_api_user.c \ + ${SRC}/ext/rcheevos/src/client/rc_client.c \ + ${SRC}/ext/rcheevos/src/client/rc_client_async.c \ + ${SRC}/ext/rcheevos/src/client/rc_client_do_frame.c \ + ${SRC}/ext/rcheevos/src/client/rc_client_external.c \ + ${SRC}/ext/rcheevos/src/client/rc_client_game.c \ + ${SRC}/ext/rcheevos/src/client/rc_client_info.c \ + ${SRC}/ext/rcheevos/src/client/rc_client_load.c \ + ${SRC}/ext/rcheevos/src/client/rc_client_raintegration.c \ + ${SRC}/ext/rcheevos/src/client/rc_client_server.c \ + ${SRC}/ext/rcheevos/src/client/rc_client_user.c \ + ${SRC}/ext/rcheevos/src/runtime/rc_alloc.c \ + ${SRC}/ext/rcheevos/src/runtime/rc_condition.c \ + ${SRC}/ext/rcheevos/src/runtime/rc_condset.c \ + ${SRC}/ext/rcheevos/src/runtime/rc_consoleinfo.c \ + ${SRC}/ext/rcheevos/src/runtime/rc_format.c \ + ${SRC}/ext/rcheevos/src/runtime/rc_lboard.c \ + ${SRC}/ext/rcheevos/src/runtime/rc_memref.c \ + ${SRC}/ext/rcheevos/src/runtime/rc_operand.c \ + ${SRC}/ext/rcheevos/src/runtime/rc_richpresence.c \ + ${SRC}/ext/rcheevos/src/runtime/rc_runtime.c \ + ${SRC}/ext/rcheevos/src/runtime/rc_runtime_progress.c \ + ${SRC}/ext/rcheevos/src/runtime/rc_trigger.c \ + ${SRC}/ext/rcheevos/src/runtime/rc_typed_value.c \ + ${SRC}/ext/rcheevos/src/runtime/rc_value.c \ + ${SRC}/ext/rcheevos/src/util/md5.c \ + ${SRC}/ext/rcheevos/src/util/rc_compat.c \ + ${SRC}/ext/rcheevos/src/util/rc_util.c \ + ${SRC}/ext/rcheevos/src/util/rc_version.c ifeq ($(TARGET_ARCH_ABI),arm64-v8a) ADRENOTOOLS_FILES := \ diff --git a/ext/rcheevos-build/CMakeLists.txt b/ext/rcheevos-build/CMakeLists.txt index 80d000b52b..6b0dfda806 100644 --- a/ext/rcheevos-build/CMakeLists.txt +++ b/ext/rcheevos-build/CMakeLists.txt @@ -3,44 +3,76 @@ project (RCheevos) set(SRC_DIR ../rcheevos/src) -# format set(ALL_SOURCE_FILES - # rapi - ${SRC_DIR}/rapi/rc_api_common.c - ${SRC_DIR}/rapi/rc_api_common.h - ${SRC_DIR}/rapi/rc_api_editor.c - ${SRC_DIR}/rapi/rc_api_info.c - ${SRC_DIR}/rapi/rc_api_runtime.c - ${SRC_DIR}/rapi/rc_api_user.c - # rcheevos - ${SRC_DIR}/rcheevos/alloc.c - ${SRC_DIR}/rcheevos/condition.c - ${SRC_DIR}/rcheevos/condset.c - ${SRC_DIR}/rcheevos/consoleinfo.c - ${SRC_DIR}/rcheevos/format.c - ${SRC_DIR}/rcheevos/lboard.c - ${SRC_DIR}/rcheevos/memref.c - ${SRC_DIR}/rcheevos/operand.c - ${SRC_DIR}/rc_compat.h - ${SRC_DIR}/rc_compat.c - ${SRC_DIR}/rc_util.c - ${SRC_DIR}/rc_client.c - ${SRC_DIR}/rc_client_internal.h - ${SRC_DIR}/rc_version.h - ${SRC_DIR}/rcheevos/rc_internal.h - ${SRC_DIR}/rcheevos/rc_validate.h - ${SRC_DIR}/rcheevos/rc_validate.c - ${SRC_DIR}/rcheevos/richpresence.c - ${SRC_DIR}/rcheevos/runtime.c - ${SRC_DIR}/rcheevos/runtime_progress.c - ${SRC_DIR}/rcheevos/trigger.c - ${SRC_DIR}/rcheevos/value.c - # rhash (still need just md5, hash) - ${SRC_DIR}/rhash/md5.c - ${SRC_DIR}/rhash/md5.h + # api + ${SRC_DIR}/api/rc_api_common.c + ${SRC_DIR}/api/rc_api_editor.c + ${SRC_DIR}/api/rc_api_info.c + ${SRC_DIR}/api/rc_api_runtime.c + ${SRC_DIR}/api/rc_api_user.c + ${SRC_DIR}/api/rc_api_common.h + # client + ${SRC_DIR}/client/rc_client.c + ${SRC_DIR}/client/rc_client_async.c + ${SRC_DIR}/client/rc_client_do_frame.c + ${SRC_DIR}/client/rc_client_external.c + ${SRC_DIR}/client/rc_client_game.c + ${SRC_DIR}/client/rc_client_info.c + ${SRC_DIR}/client/rc_client_load.c + ${SRC_DIR}/client/rc_client_raintegration.c + ${SRC_DIR}/client/rc_client_server.c + ${SRC_DIR}/client/rc_client_user.c + ${SRC_DIR}/client/rc_client_async.h + ${SRC_DIR}/client/rc_client_do_frame.h + ${SRC_DIR}/client/rc_client_external.h + ${SRC_DIR}/client/rc_client_external_versions.h + ${SRC_DIR}/client/rc_client_game.h + ${SRC_DIR}/client/rc_client_internal.h + ${SRC_DIR}/client/rc_client_load.h + ${SRC_DIR}/client/rc_client_raintegration_internal.h + ${SRC_DIR}/client/rc_client_server.h + # runtime + ${SRC_DIR}/runtime/rc_alloc.c + ${SRC_DIR}/runtime/rc_condition.c + ${SRC_DIR}/runtime/rc_condset.c + ${SRC_DIR}/runtime/rc_consoleinfo.c + ${SRC_DIR}/runtime/rc_format.c + ${SRC_DIR}/runtime/rc_lboard.c + ${SRC_DIR}/runtime/rc_memref.c + ${SRC_DIR}/runtime/rc_operand.c + ${SRC_DIR}/runtime/rc_richpresence.c + ${SRC_DIR}/runtime/rc_runtime.c + ${SRC_DIR}/runtime/rc_runtime_progress.c + ${SRC_DIR}/runtime/rc_trigger.c + ${SRC_DIR}/runtime/rc_typed_value.c + ${SRC_DIR}/runtime/rc_value.c + ${SRC_DIR}/runtime/rc_alloc.h + ${SRC_DIR}/runtime/rc_condition.h + ${SRC_DIR}/runtime/rc_condset.h + ${SRC_DIR}/runtime/rc_eval_state.h + ${SRC_DIR}/runtime/rc_format.h + ${SRC_DIR}/runtime/rc_lboard.h + ${SRC_DIR}/runtime/rc_memref.h + ${SRC_DIR}/runtime/rc_modified_memref.h + ${SRC_DIR}/runtime/rc_operand.h + ${SRC_DIR}/runtime/rc_operator.h + ${SRC_DIR}/runtime/rc_parse_state.h + ${SRC_DIR}/runtime/rc_richpresence.h + ${SRC_DIR}/runtime/rc_trigger.h + ${SRC_DIR}/runtime/rc_typed_value.h + ${SRC_DIR}/runtime/rc_validate.h + ${SRC_DIR}/runtime/rc_value.h + # util + ${SRC_DIR}/util/md5.c + ${SRC_DIR}/util/rc_compat.c + ${SRC_DIR}/util/rc_util.c + ${SRC_DIR}/util/rc_version.c + ${SRC_DIR}/util/md5.h + ${SRC_DIR}/util/rc_compat.h + ${SRC_DIR}/util/rc_version.h ) add_library(rcheevos STATIC ${ALL_SOURCE_FILES}) target_compile_definitions(rcheevos PUBLIC RC_DISABLE_LUA) -target_include_directories(rcheevos PUBLIC ../rcheevos/include ../rcheevos/src/rapi) +target_include_directories(rcheevos PUBLIC ../rcheevos/include) diff --git a/ext/rcheevos-build/rcheevos.vcxproj b/ext/rcheevos-build/rcheevos.vcxproj index a9fa7f0636..d62197a8d5 100644 --- a/ext/rcheevos-build/rcheevos.vcxproj +++ b/ext/rcheevos-build/rcheevos.vcxproj @@ -27,58 +27,88 @@ - - - - - - - - - - - - - - - - - - - - - + + + + + + + + + true true - - - - + + + + + + + + + + + + + + + + + + + + + + + + - + + + - - - - - - - - - - + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + 16.0 diff --git a/ext/rcheevos-build/rcheevos.vcxproj.filters b/ext/rcheevos-build/rcheevos.vcxproj.filters index b8b95238e9..ed83ab9055 100644 --- a/ext/rcheevos-build/rcheevos.vcxproj.filters +++ b/ext/rcheevos-build/rcheevos.vcxproj.filters @@ -1,100 +1,124 @@  - - {d69c6a56-d9c2-4b21-96af-7adc52a4e0ea} - - - {0acc1fc7-94ca-4eee-967e-5a64a884dbf1} - - {9e049d1f-4b83-4aa5-89f3-01a42e1773e2} + {8ff1718b-3f8b-4441-8397-40210d1e157b} - - {e6d16c54-0892-4ed2-8b51-ab2f5bd8e2d3} + + {b0a9b5c0-f38e-445a-8824-ba1c1ff8c0c1} + + + {17ecde1c-085c-44a4-ae21-67cc0c98bad4} + + + {d0d210fe-6160-4596-bab4-f8713d548ea3} + + + {134f3193-8fb0-417e-b506-799a24874ed1} - - rapi + + api - - rapi + + api - - rapi + + api - - rapi + + api - - rapi + + api - - rcheevos + + client - - rcheevos + + client - - rcheevos + + client - - rcheevos + + client - - rcheevos + + client - - rcheevos + + client - - rcheevos + + client - - rcheevos + + client - - rcheevos + + client - - rcheevos + + client - - rcheevos + + runtime - - rcheevos + + runtime - - rcheevos + + runtime - - rcheevos + + runtime - - rcheevos + + runtime - - rcheevos + + runtime - - rcheevos + + runtime - - rhash + + runtime - - rcheevos + + runtime - - rcheevos + + runtime + + + runtime + + + runtime + + + runtime + + + runtime + + + util + + + util + + + util + + + util - - rapi - include @@ -110,53 +134,122 @@ include + + include + + + include + + + include + include include + + include + include include - + include include - - rcheevos + + api - - rcheevos + + client - - include + + client - - rcheevos + + client - - rcheevos + + client - - rcheevos + + client - - include + + client - - rhash + + client - - rcheevos + + client - - rcheevos + + client + + + runtime + + + runtime + + + runtime + + + runtime + + + runtime + + + runtime + + + runtime + + + runtime + + + runtime + + + runtime + + + runtime + + + runtime + + + runtime + + + runtime + + + runtime + + + runtime + + + util + + + util + + + util \ No newline at end of file diff --git a/libretro/Makefile.common b/libretro/Makefile.common index f53945c380..38cfedceb4 100644 --- a/libretro/Makefile.common +++ b/libretro/Makefile.common @@ -238,29 +238,39 @@ INCFLAGS += -I$(EXTDIR)/rcheevos/include COREFLAGS += -DRC_DISABLE_LUA SOURCES_C += \ - $(EXTDIR)/rcheevos/src/rapi/rc_api_common.c \ - $(EXTDIR)/rcheevos/src/rapi/rc_api_editor.c \ - $(EXTDIR)/rcheevos/src/rapi/rc_api_info.c \ - $(EXTDIR)/rcheevos/src/rapi/rc_api_runtime.c \ - $(EXTDIR)/rcheevos/src/rapi/rc_api_user.c \ - $(EXTDIR)/rcheevos/src/rcheevos/alloc.c \ - $(EXTDIR)/rcheevos/src/rcheevos/condition.c \ - $(EXTDIR)/rcheevos/src/rcheevos/condset.c \ - $(EXTDIR)/rcheevos/src/rcheevos/consoleinfo.c \ - $(EXTDIR)/rcheevos/src/rcheevos/format.c \ - $(EXTDIR)/rcheevos/src/rcheevos/lboard.c \ - $(EXTDIR)/rcheevos/src/rcheevos/memref.c \ - $(EXTDIR)/rcheevos/src/rcheevos/operand.c \ - $(EXTDIR)/rcheevos/src/rc_client.c \ - $(EXTDIR)/rcheevos/src/rc_util.c \ - $(EXTDIR)/rcheevos/src/rc_compat.c \ - $(EXTDIR)/rcheevos/src/rcheevos/rc_validate.c \ - $(EXTDIR)/rcheevos/src/rcheevos/richpresence.c \ - $(EXTDIR)/rcheevos/src/rcheevos/runtime.c \ - $(EXTDIR)/rcheevos/src/rcheevos/runtime_progress.c \ - $(EXTDIR)/rcheevos/src/rcheevos/trigger.c \ - $(EXTDIR)/rcheevos/src/rcheevos/value.c \ - $(EXTDIR)/rcheevos/src/rhash/md5.c + $(EXTDIR)/rcheevos/src/api/rc_api_common.c \ + $(EXTDIR)/rcheevos/src/api/rc_api_editor.c \ + $(EXTDIR)/rcheevos/src/api/rc_api_info.c \ + $(EXTDIR)/rcheevos/src/api/rc_api_runtime.c \ + $(EXTDIR)/rcheevos/src/api/rc_api_user.c \ + $(EXTDIR)/rcheevos/src/client/rc_client.c \ + $(EXTDIR)/rcheevos/src/client/rc_client_async.c \ + $(EXTDIR)/rcheevos/src/client/rc_client_do_frame.c \ + $(EXTDIR)/rcheevos/src/client/rc_client_external.c \ + $(EXTDIR)/rcheevos/src/client/rc_client_game.c \ + $(EXTDIR)/rcheevos/src/client/rc_client_info.c \ + $(EXTDIR)/rcheevos/src/client/rc_client_load.c \ + $(EXTDIR)/rcheevos/src/client/rc_client_raintegration.c \ + $(EXTDIR)/rcheevos/src/client/rc_client_server.c \ + $(EXTDIR)/rcheevos/src/client/rc_client_user.c \ + $(EXTDIR)/rcheevos/src/runtime/rc_alloc.c \ + $(EXTDIR)/rcheevos/src/runtime/rc_condition.c \ + $(EXTDIR)/rcheevos/src/runtime/rc_condset.c \ + $(EXTDIR)/rcheevos/src/runtime/rc_consoleinfo.c \ + $(EXTDIR)/rcheevos/src/runtime/rc_format.c \ + $(EXTDIR)/rcheevos/src/runtime/rc_lboard.c \ + $(EXTDIR)/rcheevos/src/runtime/rc_memref.c \ + $(EXTDIR)/rcheevos/src/runtime/rc_operand.c \ + $(EXTDIR)/rcheevos/src/runtime/rc_richpresence.c \ + $(EXTDIR)/rcheevos/src/runtime/rc_runtime.c \ + $(EXTDIR)/rcheevos/src/runtime/rc_runtime_progress.c \ + $(EXTDIR)/rcheevos/src/runtime/rc_trigger.c \ + $(EXTDIR)/rcheevos/src/runtime/rc_typed_value.c \ + $(EXTDIR)/rcheevos/src/runtime/rc_value.c \ + $(EXTDIR)/rcheevos/src/util/md5.c \ + $(EXTDIR)/rcheevos/src/util/rc_compat.c \ + $(EXTDIR)/rcheevos/src/util/rc_util.c \ + $(EXTDIR)/rcheevos/src/util/rc_version.c COREFLAGS += -DSTACK_LINE_READER_BUFFER_SIZE=1024 COREFLAGS += -DHTTPS_NOT_AVAILABLE From d756893c1c59e291faafa3f53411ad3733a44e1d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Henrik=20Rydg=C3=A5rd?= Date: Thu, 5 Nov 2026 18:48:11 +0100 Subject: [PATCH 06/14] UI: Give RadioButton the same padding and height as CheckBox Co-Authored-By: Claude Opus 5.5 --- Common/UI/View.cpp | 4 ++-- Common/UI/View.h | 4 ++-- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/Common/UI/View.cpp b/Common/UI/View.cpp index 637709a296..3861adc1ca 100644 --- a/Common/UI/View.cpp +++ b/Common/UI/View.cpp @@ -1075,9 +1075,9 @@ void RadioButton::GetContentDimensions(const UIContext &dc, float &w, float &h) dc.MeasureText(dc.GetTheme().uiFont, 1.0f, 1.0f, text_, &w, &h); } - // Add some internal padding to not look totally ugly + // Padding on both sides, and between the radio image and the text. Same height as a CheckBox. w += paddingW_ * 3.0f + radioRadius_ * 2.0f; - h = std::max(h, radioRadius_ * 2) + paddingH_ * 2; + h = std::max(std::max(h, radioRadius_ * 2), ITEM_HEIGHT); } std::string RadioButton::DescribeText() const { diff --git a/Common/UI/View.h b/Common/UI/View.h index d3322280aa..ee961d25ab 100644 --- a/Common/UI/View.h +++ b/Common/UI/View.h @@ -1011,8 +1011,8 @@ private: int *value_; int thisButtonValue_; std::string text_; - const float paddingW_ = 8; - const float paddingH_ = 4; + // Same as CheckBox. + const float paddingW_ = 12; // Half the size of the I_RADIO_* images. const float radioRadius_ = 16.0f; From bcfcdb997408732b6d85a49d6016e8fdf74bcae5 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Henrik=20Rydg=C3=A5rd?= Date: Thu, 5 Nov 2026 20:03:37 +0100 Subject: [PATCH 07/14] Improve the look of the MemstickScreen --- Core/HLE/sceUtility.cpp | 35 +++++++ UI/MainScreen.cpp | 2 + UI/MemStickScreen.cpp | 194 +++++++++++++++++++------------------- UI/MemStickScreen.h | 9 +- UI/SimpleDialogScreen.cpp | 6 ++ UI/SimpleDialogScreen.h | 1 + 6 files changed, 145 insertions(+), 102 deletions(-) diff --git a/Core/HLE/sceUtility.cpp b/Core/HLE/sceUtility.cpp index cdfcf2ad7b..c36e32817c 100644 --- a/Core/HLE/sceUtility.cpp +++ b/Core/HLE/sceUtility.cpp @@ -86,6 +86,11 @@ static const int atrac3PlusModuleDeps[] = {0x0300, 0}; static const int mpegBaseModuleDeps[] = {0x0300, 0}; static const int mp4ModuleDeps[] = {0x0300, 0}; +static u32 vshHtmlViewerWorkspaceAddr; +static u32 vshHtmlViewerWorkspaceSize; + +static constexpr const char *VSH_HTMLVIEWER_WORKSPACE_TAG = "VSH htmlviewer workspace"; + // Loading the firmware's module for a library we've been told not to HLE. // // This is the path for a game that brings no copy of its own. One that does loads it directly and @@ -1782,6 +1787,34 @@ static u32 sceUtilityUnloadUsbModule(u32 module) { return hleLogWarning(Log::sceUtility, 0, "UNIMPL"); } +static u32 VshHtmlViewerAcquireWorkspace(u32 stateAddr, u32 ownerAddr) { + if (!Memory::IsValidRange(stateAddr, 12)) + return 0; + + u32 workspaceAddr = Memory::ReadUnchecked_U32(stateAddr); + u32 workspaceSize = Memory::ReadUnchecked_U32(stateAddr + 4); + + if (workspaceAddr == 0) { + workspaceSize = + Memory::g_MemorySize > 0x023FFFFF ? 0x00C00000 : 0x00800000; + + workspaceAddr = userMemory.Alloc( + workspaceSize, false, VSH_HTMLVIEWER_WORKSPACE_TAG); + + if (workspaceAddr == (u32)-1) + return 0; + + vshHtmlViewerWorkspaceAddr = workspaceAddr; + vshHtmlViewerWorkspaceSize = workspaceSize; + + Memory::WriteUnchecked_U32(workspaceAddr, stateAddr); + Memory::WriteUnchecked_U32(workspaceSize, stateAddr + 4); + } + + Memory::WriteUnchecked_U32(ownerAddr, stateAddr + 8); + return workspaceAddr; +} + const HLEFunction sceUtility[] = { {0X1579A159, &WrapU_U, "sceUtilityLoadNetModule", 'x', "x" }, {0X64D50C56, &WrapU_U, "sceUtilityUnloadNetModule", 'x', "x" }, @@ -1927,6 +1960,8 @@ const HLEFunction sceUtility[] = { {0xC0DE0001, &WrapI_I, "__UtilityFinishDialog", 'i', "i" }, {0xC0DE0002, &WrapI_I, "__UtilityWorkUs", 'i', "i" }, {0xC0DE0003, &WrapI_I, "__UtilityInitDialog", 'i', "i" }, + + {0xC0DE0004, &WrapU_UU, "__VshHtmlViewerAcquireWorkspace", 'x', "xx"}, }; void Register_sceUtility() diff --git a/UI/MainScreen.cpp b/UI/MainScreen.cpp index deb7c7f67a..4546b094db 100644 --- a/UI/MainScreen.cpp +++ b/UI/MainScreen.cpp @@ -538,6 +538,8 @@ void MainScreen::CreateViews() { LinearLayout *buttonGroup = new LinearLayout(ORIENT_HORIZONTAL, new LinearLayoutParams(WRAP_CONTENT, WRAP_CONTENT, 1.0f, UI::Gravity::G_VCENTER)); + buttonGroup->SetPadding(Margins(0, 0, 12, 12)); + CreateMainButtons(buttonGroup, vertical); header->Add(buttonGroup); diff --git a/UI/MemStickScreen.cpp b/UI/MemStickScreen.cpp index c3f2c2bdfa..e23667e756 100644 --- a/UI/MemStickScreen.cpp +++ b/UI/MemStickScreen.cpp @@ -41,19 +41,14 @@ #include "Common/File/DiskFree.h" #include "Common/Thread/ThreadManager.h" -#include "Common/UI/ScrollView.h" #include "Common/UI/Notice.h" #include "Core/Config.h" -#include "Core/Reporting.h" -#include "Core/System.h" -#include "Core/Util/GameManager.h" #include "Core/Util/MemStick.h" #include "UI/MemStickScreen.h" #include "UI/MainScreen.h" #include "UI/BaseScreens.h" -#include "UI/OnScreenDisplay.h" static std::string FormatSpaceString(int64_t space) { if (space >= 0) { @@ -66,7 +61,9 @@ static std::string FormatSpaceString(int64_t space) { } MemStickScreen::MemStickScreen(bool initialSetup) - : initialSetup_(initialSetup) { + : UITwoPaneBaseDialogScreen(Path(), + TwoPaneFlags::SettingsToTheRight | TwoPaneFlags::ContentsCanScroll | (initialSetup ? TwoPaneFlags::NoBackButton : TwoPaneFlags::Default)), + initialSetup_(initialSetup) { #if PPSSPP_PLATFORM(ANDROID) // Let's only offer the browse-for-folder choice on Android 10 or later. // Earlier versions often don't really have working folder browsers. @@ -81,7 +78,7 @@ MemStickScreen::MemStickScreen(bool initialSetup) if (System_GetPropertyBool(SYSPROP_ANDROID_SCOPED_STORAGE)) { choice_ = CHOICE_BROWSE_FOLDER; } else { - WARN_LOG_REPORT(Log::System, "Scoped storage not enabled - shouldn't be in MemStickScreen at initial setup"); + WARN_LOG(Log::System, "Scoped storage not enabled - shouldn't be in MemStickScreen at initial setup"); choice_ = CHOICE_STORAGE_ROOT; // Shouldn't really be here in initial setup. } @@ -103,112 +100,111 @@ MemStickScreen::MemStickScreen(bool initialSetup) } } -static void AddExplanation(UI::ViewGroup *viewGroup, MemStickScreen::Choice choice, UI::View *extraView = nullptr) { +static std::string GetExplanationString(MemStickScreen::Choice choice) { auto iz = GetI18NCategory(I18NCat::MEMSTICK); using namespace UI; int flags = FLAG_WRAP_TEXT; - UI::ViewGroup *holder = new UI::LinearLayout(ORIENT_VERTICAL); - - UI::ViewGroup *indentHolder = new UI::LinearLayout(ORIENT_HORIZONTAL); - indentHolder->Add(new Spacer(20.0)); - indentHolder->Add(holder); - - viewGroup->Add(indentHolder); - - if (extraView) { - holder->Add(extraView); - } - + std::string str; + std::string_view bullet = "• "; switch (choice) { case MemStickScreen::CHOICE_STORAGE_ROOT: // Old school choice - holder->Add(new TextView(iz->T("DataWillStay", "Data will stay even if you uninstall PPSSPP"), flags, false))->SetBullet(true); - holder->Add(new TextView(iz->T("DataCanBeShared", "Data can be shared between PPSSPP regular/Gold"), flags, false))->SetBullet(true); - holder->Add(new TextView(iz->T("EasyUSBAccess", "Easy USB access"), flags, false))->SetBullet(true); + str += bullet; + str += iz->T("DataWillStay", "Data will stay even if you uninstall PPSSPP"); + str += "\n"; + str += bullet; + str += iz->T("DataCanBeShared", "Data can be shared between PPSSPP regular/Gold"); + str += "\n"; + str += bullet; + str += iz->T("EasyUSBAccess", "Easy USB access"); + str += "\n"; break; case MemStickScreen::CHOICE_BROWSE_FOLDER: - holder->Add(new TextView(iz->T("DataWillStay", "Data will stay even if you uninstall PPSSPP"), flags, false))->SetBullet(true); - holder->Add(new TextView(iz->T("DataCanBeShared", "Data can be shared between PPSSPP regular/Gold"), flags, false))->SetBullet(true); -#if !PPSSPP_PLATFORM(UWP) - holder->Add(new TextView(iz->T("EasyUSBAccess", "Easy USB access"), flags, false))->SetBullet(true); -#endif + str += bullet; + str += iz->T("DataWillStay", "Data will stay even if you uninstall PPSSPP"); + str += "\n"; + str += bullet; + str += iz->T("DataCanBeShared", "Data can be shared between PPSSPP regular/Gold"); + str += "\n"; + str += bullet; + str += iz->T("EasyUSBAccess", "Easy USB access"); + str += "\n"; break; case MemStickScreen::CHOICE_PRIVATE_DIRECTORY: // Consider https://www.compart.com/en/unicode/U+26A0 (unicode warning sign?)? or a graphic? - holder->Add(new TextView(iz->T("DataWillBeLostOnUninstall", "Warning! Data will be lost when you uninstall PPSSPP!"), flags, false))->SetBullet(true); - holder->Add(new TextView(iz->T("DataCannotBeShared", "Data CANNOT be shared between PPSSPP regular/Gold!"), flags, false))->SetBullet(true); + str += bullet; + str += iz->T("DataWillBeLostOnUninstall", "Warning! Data will be lost when you uninstall PPSSPP!"); + str += "\n"; + str += bullet; + str += iz->T("DataCannotBeShared", "Data CANNOT be shared between PPSSPP regular/Gold!"); + str += "\n"; #if !PPSSPP_PLATFORM(UWP) if (System_GetPropertyBool(SYSPROP_APP_GOLD)) { - holder->Add(new TextView(iz->T("USBAccessThroughGold", "USB access through Android/data/org.ppsspp.ppssppgold/files"), flags, false))->SetBullet(true); + str += bullet; + str += iz->T("USBAccessThroughGold", "USB access through Android/data/org.ppsspp.ppssppgold/files"); + str += "\n"; } else { - holder->Add(new TextView(iz->T("USBAccessThrough", "USB access through Android/data/org.ppsspp.ppsspp/files"), flags, false))->SetBullet(true); + str += bullet; + str += iz->T("USBAccessThrough", "USB access through Android/data/org.ppsspp.ppsspp/files"); + str += "\n"; } #endif break; case MemStickScreen::CHOICE_SET_MANUAL: - holder->Add(new TextView(iz->T("DataWillStay", "Data will stay even if you uninstall PPSSPP"), flags, false))->SetBullet(true); - holder->Add(new TextView(iz->T("EasyUSBAccess", "Easy USB access"), flags, false))->SetBullet(true); + str += bullet; + str += iz->T("DataWillStay", "Data will stay even if you uninstall PPSSPP"); + str += "\n"; + str += bullet; + str += iz->T("EasyUSBAccess", "Easy USB access"); + str += "\n"; break; default: - holder->Add(new TextView(iz->T("EasyUSBAccess", "Easy USB access"), flags, false))->SetBullet(true); + str += bullet; + str += iz->T("EasyUSBAccess", "Easy USB access"); + str += "\n"; break; } + // The last line's newline would leave an empty line at the bottom of the hint. + while (!str.empty() && str.back() == '\n') { + str.pop_back(); + } + return str; } -void MemStickScreen::CreateViews() { +std::string_view MemStickScreen::GetTitle() const { + if (initialSetup_) { + return T(I18NCat::MEMSTICK, "Welcome to PPSSPP!"); + } + return T(I18NCat::SYSTEM, "Memory Stick folder"); +} + +void MemStickScreen::CreateContentViews(UI::ViewGroup *parent) { using namespace UI; - auto di = GetI18NCategory(I18NCat::DIALOG); auto ms = GetI18NCategory(I18NCat::MEMSTICK); - Margins actionMenuMargins(15, 0, 15, 0); + ChoiceGroupList *groups = parent->Add(new ChoiceGroupList()); - root_ = new LinearLayout(ORIENT_HORIZONTAL); + groups->Add(new TextView(ms->T("MemoryStickDescription", "Choose where to keep PSP data (Memory Stick)"), ALIGN_LEFT | FLAG_WRAP_TEXT, false)); - Spacer *spacerColumn = new Spacer(new LinearLayoutParams(20.0, FILL_PARENT, 0.0f)); - ScrollView *mainColumnScroll = new ScrollView(ORIENT_VERTICAL, new LinearLayoutParams(1.0)); - - ViewGroup *mainColumn = new LinearLayoutList(ORIENT_VERTICAL); - mainColumnScroll->Add(mainColumn); - - root_->Add(spacerColumn); - root_->Add(mainColumnScroll); - - if (initialSetup_) { - mainColumn->Add(new Spacer(new LinearLayoutParams(FILL_PARENT, 12.0f, 0.0f))); - mainColumn->Add(new TextView(ms->T("Welcome to PPSSPP!"), ALIGN_LEFT, false)); - } - - mainColumn->Add(new Spacer(new LinearLayoutParams(FILL_PARENT, 18.0f, 0.0f))); - - mainColumn->Add(new TextView(ms->T("MemoryStickDescription", "Choose where to keep PSP data (Memory Stick)"), ALIGN_LEFT, false)); - mainColumn->Add(new Spacer(new LinearLayoutParams(FILL_PARENT, 18.0f, 0.0f))); - - ViewGroup *subColumns = new LinearLayoutList(ORIENT_HORIZONTAL); - mainColumn->Add(subColumns); - - ViewGroup *leftColumn = new LinearLayoutList(ORIENT_VERTICAL, new LinearLayoutParams(1.0)); - subColumns->Add(leftColumn); - - LinearLayout *rightColumnItems = new LinearLayout(ORIENT_VERTICAL, new LinearLayoutParams(250, FILL_PARENT, actionMenuMargins)); - subColumns->Add(rightColumnItems); + ChoiceGroup *choiceGroup = groups->Add(new ChoiceGroup()); // For legacy Android systems, so you can switch back to the old ways if you move to SD or something. // Trying to avoid needing a scroll view, so only showing the explanation for one option at a time. #if !PPSSPP_PLATFORM(UWP) if (!System_GetPropertyBool(SYSPROP_ANDROID_SCOPED_STORAGE)) { - leftColumn->Add(new RadioButton(&choice_, CHOICE_STORAGE_ROOT, ms->T("Use PSP folder at root of storage")))->OnClick.Handle(this, &MemStickScreen::OnChoiceClick); - if (choice_ == CHOICE_STORAGE_ROOT) { - AddExplanation(leftColumn, (MemStickScreen::Choice)choice_); - } + RadioButton *pspFolder = choiceGroup->Add(new RadioButton(&choice_, CHOICE_STORAGE_ROOT, ms->T("Use PSP folder at root of storage"))); + pspFolder->OnClick.Handle(this, &MemStickScreen::OnChoiceClick); + pspFolder->SetHintText(GetExplanationString(CHOICE_STORAGE_ROOT)); } #endif if (storageBrowserWorking_) { - leftColumn->Add(new RadioButton(&choice_, CHOICE_BROWSE_FOLDER, ms->T("Create or Choose a PSP folder")))->OnClick.Handle(this, &MemStickScreen::OnChoiceClick); - + RadioButton *browseFolder = choiceGroup->Add(new RadioButton(&choice_, CHOICE_BROWSE_FOLDER, ms->T("Create or Choose a PSP folder"))); + browseFolder->OnClick.Handle(this, &MemStickScreen::OnChoiceClick); + browseFolder->SetHintText(GetExplanationString(CHOICE_BROWSE_FOLDER)); // TODO: Show current folder here if we have one set. } @@ -219,16 +215,9 @@ void MemStickScreen::CreateViews() { #endif if (!storageBrowserWorking_ || legacy) { - leftColumn->Add(new RadioButton(&choice_, CHOICE_SET_MANUAL, ms->T("Manually specify PSP folder")))->OnClick.Handle(this, &MemStickScreen::OnChoiceClick); - // TODO: Show current folder here if we have one set. - } - - if (choice_ == CHOICE_BROWSE_FOLDER || choice_ == CHOICE_SET_MANUAL) { - UI::View *extraView = nullptr; - if (!g_Config.memStickDirectory.empty()) { - extraView = new TextView(StringFromFormat(" %s: %s", ms->T_cstr("Current"), g_Config.memStickDirectory.ToVisualString().c_str()), ALIGN_LEFT, false); - } - AddExplanation(leftColumn, (MemStickScreen::Choice)choice_, extraView); + RadioButton *manualFolder = choiceGroup->Add(new RadioButton(&choice_, CHOICE_SET_MANUAL, ms->T("Manually specify PSP folder"))); + manualFolder->OnClick.Handle(this, &MemStickScreen::OnChoiceClick); + manualFolder->SetHintText(GetExplanationString(CHOICE_SET_MANUAL)); } std::string privateString(ms->T("Use App Private Data")); @@ -237,12 +226,22 @@ void MemStickScreen::CreateViews() { privateString = StringFromFormat("%s (%s)", ms->T_cstr("Skip for now"), privateString.c_str()); } - leftColumn->Add(new RadioButton(&choice_, CHOICE_PRIVATE_DIRECTORY, privateString))->OnClick.Handle(this, &MemStickScreen::OnChoiceClick); - if (choice_ == CHOICE_PRIVATE_DIRECTORY) { - AddExplanation(leftColumn, (MemStickScreen::Choice)choice_); + RadioButton *privateFolder = choiceGroup->Add(new RadioButton(&choice_, CHOICE_PRIVATE_DIRECTORY, privateString)); + privateFolder->OnClick.Handle(this, &MemStickScreen::OnChoiceClick); + privateFolder->SetHintText(GetExplanationString(CHOICE_PRIVATE_DIRECTORY)); + + if ((choice_ == CHOICE_BROWSE_FOLDER || choice_ == CHOICE_SET_MANUAL) && !g_Config.memStickDirectory.empty()) { + groups->Add(new TextView(StringFromFormat("%s: %s", ms->T_cstr("Current"), g_Config.memStickDirectory.ToVisualString().c_str()), ALIGN_LEFT, false)); } - leftColumn->Add(new Spacer(new LinearLayoutParams(FILL_PARENT, 12.0f, 0.0f))); + INFO_LOG(Log::System, "MemStickScreen: initialSetup=%d", (int)initialSetup_); +} + +void MemStickScreen::CreateSettingsViews(UI::ViewGroup *parent) { + using namespace UI; + + auto di = GetI18NCategory(I18NCat::DIALOG); + auto ms = GetI18NCategory(I18NCat::MEMSTICK); std::string_view confirmButtonText; ImageID confirmButtonImage = ImageID::invalid(); @@ -266,19 +265,16 @@ void MemStickScreen::CreateViews() { break; } - rightColumnItems->Add(new UI::Choice(confirmButtonText, confirmButtonImage))->OnClick.Handle(this, &MemStickScreen::OnConfirmClick); - rightColumnItems->Add(new Spacer(new LinearLayoutParams(FILL_PARENT, 12.0f, 0.0f))); + ChoiceGroupList *groups = parent->Add(new ChoiceGroupList()); - if (!initialSetup_) { - rightColumnItems->Add(new UI::Choice(di->T("Back")))->OnClick.Handle(this, &UIScreen::OnBack); - } + ChoiceGroup *confirmGroup = groups->Add(new ChoiceGroup()); + confirmGroup->Add(new UI::Choice(confirmButtonText, confirmButtonImage))->OnClick.Handle(this, &MemStickScreen::OnConfirmClick); + + // The top bar has the back button. if (System_GetPropertyInt(SYSPROP_DEVICE_TYPE) != DEVICE_TYPE_TV) { - rightColumnItems->Add(new UI::Choice(ms->T("WhatsThis", "What's this?"), ImageID("I_LINK_OUT_QUESTION")))->OnClick.Handle(this, &MemStickScreen::OnHelp); + ChoiceGroup *helpGroup = groups->Add(new ChoiceGroup()); + helpGroup->Add(new UI::Choice(ms->T("WhatsThis", "What's this?"), ImageID("I_LINK_OUT_QUESTION")))->OnClick.Handle(this, &MemStickScreen::OnHelp); } - - rightColumnItems->ApplyRoundCorners(); - - INFO_LOG(Log::System, "MemStickScreen: initialSetup=%d", (int)initialSetup_); } void MemStickScreen::OnHelp(UI::EventParams ¶ms) { @@ -384,7 +380,7 @@ void MemStickScreen::UseInternalStorage(UI::EventParams ¶ms) { TriggerFinish(DialogResult::DR_OK); } else { // This can't really happen?? Not worth making an error message. - ERROR_LOG_REPORT(Log::System, "Could not switch memstick path in setup (internal)"); + ERROR_LOG(Log::System, "Could not switch memstick path in setup (internal)"); } // Don't have a confirmation dialog that would otherwise do it for us, need to just switch directly to the main screen. screenManager()->switchScreen(new MainScreen()); @@ -407,7 +403,7 @@ void MemStickScreen::UseStorageRoot(UI::EventParams ¶ms) { TriggerFinish(DialogResult::DR_OK); } else { // This can't really happen?? Not worth making an error message. - ERROR_LOG_REPORT(Log::System, "Could not switch memstick path in setup"); + ERROR_LOG(Log::System, "Could not switch memstick path in setup"); } } else if (pendingMemStickFolder != g_Config.memStickDirectory) { // Always ask for confirmation when called from the UI. Likely there's already some data. @@ -466,7 +462,7 @@ void MemStickScreen::dialogFinished(const Screen *dialog, DialogResult result) { } void MemStickScreen::update() { - UIBaseDialogScreen::update(); + UITwoPaneBaseDialogScreen::update(); if (done_) { TriggerFinish(DialogResult::DR_OK); done_ = false; diff --git a/UI/MemStickScreen.h b/UI/MemStickScreen.h index 5242b665be..86a4264fa7 100644 --- a/UI/MemStickScreen.h +++ b/UI/MemStickScreen.h @@ -31,12 +31,13 @@ #include "UI/BaseScreens.h" #include "UI/MiscViews.h" +#include "UI/SimpleDialogScreen.h" class NoticeView; // MemStickScreen - let's you configure your memory stick directory. // Currently only useful for Android. -class MemStickScreen : public UIBaseDialogScreen { +class MemStickScreen : public UITwoPaneBaseDialogScreen { public: MemStickScreen(bool initialSetup); ~MemStickScreen() = default; @@ -51,7 +52,9 @@ public: }; protected: - void CreateViews() override; + void CreateContentViews(UI::ViewGroup *parent) override; + void CreateSettingsViews(UI::ViewGroup *parent) override; + std::string_view GetTitle() const override; void dialogFinished(const Screen *dialog, DialogResult result) override; void update() override; @@ -59,7 +62,7 @@ protected: // Simple anti-flicker due to delayed finish. if (!done_) { // render as usual. - return UIBaseDialogScreen::render(mode); + return UITwoPaneBaseDialogScreen::render(mode); } else { // no render. black frame insertion is better than flicker. } diff --git a/UI/SimpleDialogScreen.cpp b/UI/SimpleDialogScreen.cpp index 0d6d52bf74..f831fedc70 100644 --- a/UI/SimpleDialogScreen.cpp +++ b/UI/SimpleDialogScreen.cpp @@ -73,6 +73,9 @@ void UITwoPaneBaseDialogScreen::CreateViews() { if (flags_ & TwoPaneFlags::SettingsToTheRight) { // If settings are in context menu, we want to avoid double margins on the sides. margins.left = 0; + if (portrait) { + margins.right = 0; + } } else { margins.right = 0; } @@ -96,6 +99,9 @@ void UITwoPaneBaseDialogScreen::CreateViews() { if (flags_ & TwoPaneFlags::DropShadowTitle) { topBarFlags |= TopBarFlags::DropShadowTitle; } + if (flags_ & TwoPaneFlags::NoBackButton) { + topBarFlags |= TopBarFlags::NoBackButton; + } TopBar *topBar = root->Add(new TopBar(*screenManager()->getUIContext(), topBarFlags, GetTitle())); root->SetSpacing(0); createContentViews(root); diff --git a/UI/SimpleDialogScreen.h b/UI/SimpleDialogScreen.h index bfd5ba73cf..58c060547b 100644 --- a/UI/SimpleDialogScreen.h +++ b/UI/SimpleDialogScreen.h @@ -43,6 +43,7 @@ enum class TwoPaneFlags { CustomContextMenu = 16, NoTopbarInLandscape = 32, DropShadowTitle = 64, + NoBackButton = 128, }; ENUM_CLASS_BITOPS(TwoPaneFlags); From dacccd29bb385babf54cc05c8e94c1a2702d55c6 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Henrik=20Rydg=C3=A5rd?= Date: Thu, 5 Nov 2026 21:07:27 +0100 Subject: [PATCH 08/14] UI: Wrap RadioButton's title text Measure it with MeasureTextRect against the available width, like Choice does. Co-Authored-By: Claude Opus 5.5 --- Common/UI/View.cpp | 39 ++++++++++++++++++--------------------- Common/UI/View.h | 1 + 2 files changed, 19 insertions(+), 21 deletions(-) diff --git a/Common/UI/View.cpp b/Common/UI/View.cpp index 3861adc1ca..29672953c6 100644 --- a/Common/UI/View.cpp +++ b/Common/UI/View.cpp @@ -1067,17 +1067,24 @@ void Button::Draw(UIContext &dc) { } } -void RadioButton::GetContentDimensions(const UIContext &dc, float &w, float &h) const { - w = 0.0f; - h = 0.0f; - +void RadioButton::GetContentDimensionsBySpec(const UIContext &dc, MeasureSpec horiz, MeasureSpec vert, float &w, float &h) const { + // Padding on both sides, and between the radio image and the text. + const float decorationW = paddingW_ * 3.0f + radioRadius_ * 2.0f; + float textW = 0.0f, textH = 0.0f; if (!text_.empty()) { - dc.MeasureText(dc.GetTheme().uiFont, 1.0f, 1.0f, text_, &w, &h); + float availWidth = horiz.size - decorationW; + if (horiz.type == UNSPECIFIED || availWidth <= 0.0f) { + availWidth = MAX_ITEM_SIZE; + } + dc.MeasureTextRect(dc.GetTheme().uiFont, 1.0f, 1.0f, text_, availWidth, &textW, &textH, FLAG_WRAP_TEXT); } + w = textW + decorationW; + // Same height as a CheckBox, taller if the text wraps. + h = std::max(std::max(textH + 16.0f, radioRadius_ * 2), ITEM_HEIGHT); +} - // Padding on both sides, and between the radio image and the text. Same height as a CheckBox. - w += paddingW_ * 3.0f + radioRadius_ * 2.0f; - h = std::max(std::max(h, radioRadius_ * 2), ITEM_HEIGHT); +void RadioButton::GetContentDimensions(const UIContext &dc, float &w, float &h) const { + GetContentDimensionsBySpec(dc, MeasureSpec(UNSPECIFIED), MeasureSpec(UNSPECIFIED), w, h); } std::string RadioButton::DescribeText() const { @@ -1107,22 +1114,12 @@ void RadioButton::Draw(UIContext &dc) { const ImageID image(checked ? "I_RADIO_SELECTED" : "I_RADIO_EMPTY"); dc.Draw()->DrawImage(image, bounds.x + paddingW_ + radioRadius_, bounds.centerY(), 1.0f, style.fgColor, ALIGN_CENTER); - float tw, th; - dc.MeasureText(dc.GetTheme().uiFont, 1.0f, 1.0f, text_, &tw, &th); - - if (tw > bounds.w) { - dc.PushScissor(bounds); - } - dc.SetFontStyle(dc.GetTheme().uiFont); if (!text_.empty()) { - float textX = bounds.x + paddingW_ * 2.0f + radioRadius_ * 2.0f; - dc.DrawText(text_, textX, bounds.centerY(), style.fgColor, ALIGN_LEFT | ALIGN_VCENTER); - } - - if (tw > bounds.w) { - dc.PopScissor(); + const float textX = bounds.x + paddingW_ * 2.0f + radioRadius_ * 2.0f; + const Bounds textBounds(textX, bounds.y, bounds.x2() - paddingW_ - textX, bounds.h); + dc.DrawTextRect(text_, textBounds, style.fgColor, ALIGN_LEFT | ALIGN_VCENTER | FLAG_WRAP_TEXT); } DrawHint(dc, style); diff --git a/Common/UI/View.h b/Common/UI/View.h index ee961d25ab..63f1e1f488 100644 --- a/Common/UI/View.h +++ b/Common/UI/View.h @@ -1000,6 +1000,7 @@ public: RadioButton(int *value, int thisButtonValue, std::string_view text, LayoutParams *layoutParams = nullptr) : ClickableItem(layoutParams), value_(value), thisButtonValue_(thisButtonValue), text_(text) {} void Draw(UIContext &dc) override; + void GetContentDimensionsBySpec(const UIContext &dc, MeasureSpec horiz, MeasureSpec vert, float &w, float &h) const override; void GetContentDimensions(const UIContext &dc, float &w, float &h) const override; std::string DescribeText() const override; From f039116483c5b6f0e666bda97e912f8b814f6918 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Henrik=20Rydg=C3=A5rd?= Date: Thu, 5 Nov 2026 21:14:33 +0100 Subject: [PATCH 09/14] WordWrapper: Count the space before a word when checking if it fits DirectWrite leaves trailing whitespace out of measured widths, so x_ (measured on a line ending in a space) came out one space short. A word overflowing by less than that stayed on the line, and the hard newline after it then wrapped too, leaving an empty line. Re-measure the line with the word on it, and don't wrap before a word with no width. Co-Authored-By: Claude Opus 5.5 --- Common/Data/Text/WrapText.cpp | 26 +++++++++++++++++++++++--- Common/Data/Text/WrapText.h | 3 ++- unittest/UnitTest.cpp | 24 ++++++++++++++++++++++++ 3 files changed, 49 insertions(+), 4 deletions(-) diff --git a/Common/Data/Text/WrapText.cpp b/Common/Data/Text/WrapText.cpp index 03b0a8fe60..131d4e83f6 100644 --- a/Common/Data/Text/WrapText.cpp +++ b/Common/Data/Text/WrapText.cpp @@ -79,9 +79,29 @@ std::string WordWrapper::Wrapped() { return out_; } -bool WordWrapper::WrapBeforeWord() { +bool WordWrapper::WordFitsOnLine(int endIndex) { + if (x_ + wordWidth_ > maxW_) { + return false; + } + if (lastLineStart_ >= out_.size()) { + return true; + } + // x_ was measured on a line that usually ends with a space, and some text drawers + // (DirectWrite, for one) leave trailing whitespace out of the width. Measure the + // line with the word on it. + std::string_view word = str_.substr(lastIndex_, endIndex - lastIndex_); + while (!word.empty() && (word.back() == '\n' || word.back() == ' ')) { + word.remove_suffix(1); + } + std::string line = out_.substr(lastLineStart_); + line.append(word); + return MeasureWidth(line) <= maxW_; +} + +bool WordWrapper::WrapBeforeWord(int endIndex) { if (flags_ & FLAG_WRAP_TEXT) { - if (x_ + wordWidth_ > maxW_ && !out_.empty()) { + // A word with no width (like a hard newline) has nothing to move to the next line. + if (wordWidth_ > 0.0f && !out_.empty() && !WordFitsOnLine(endIndex)) { if (IsShy(lastChar_)) { // Soft hyphen, replace it with a real hyphen since we wrapped at it. // TODO: There's an edge case here where the hyphen might not fit. @@ -125,7 +145,7 @@ void WordWrapper::AddEllipsis() { void WordWrapper::AppendWord(int endIndex, int lastChar, bool addNewline) { int lastWordStartIndex = lastIndex_; - if (WrapBeforeWord()) { + if (WrapBeforeWord(endIndex)) { // Advance to the first non-whitespace UTF-8 character in the following word (if any) to prevent starting the new line with a whitespace UTF8 utf8Word(str_, lastWordStartIndex); while (lastWordStartIndex < endIndex) { diff --git a/Common/Data/Text/WrapText.h b/Common/Data/Text/WrapText.h index 1392307aca..d3648365e0 100644 --- a/Common/Data/Text/WrapText.h +++ b/Common/Data/Text/WrapText.h @@ -17,7 +17,8 @@ protected: virtual float MeasureWidth(std::string_view str) = 0; void Wrap(); - bool WrapBeforeWord(); + bool WrapBeforeWord(int endIndex); + bool WordFitsOnLine(int endIndex); void AppendWord(int endIndex, int lastChar, bool addNewline); void AddEllipsis(); diff --git a/unittest/UnitTest.cpp b/unittest/UnitTest.cpp index 59b014aebe..c55e5d4446 100644 --- a/unittest/UnitTest.cpp +++ b/unittest/UnitTest.cpp @@ -2968,9 +2968,33 @@ protected: } }; +// Leaves trailing spaces out of the width, like DirectWrite. +class TrimmingUnitTestWordWrapper : public UnitTestWordWrapper { +public: + using UnitTestWordWrapper::UnitTestWordWrapper; + +protected: + float MeasureWidth(std::string_view str) override { + while (!str.empty() && str.back() == ' ') { + str.remove_suffix(1); + } + return UnitTestWordWrapper::MeasureWidth(str); + } +}; + #define EXPECT_WORDWRAP_EQ_STR(a, l, f, b) if (UnitTestWordWrapper(a, l, f).Wrapped() != b) { printf("%s: Test Fail (%d, %s)\n%s\nvs\n%s\n", __FUNCTION__, l, #f, UnitTestWordWrapper(a, l, f).Wrapped().c_str(), std::string(b).c_str()); return false; } static bool TestWrapText() { + // The space before "cde" must count even if the measurer drops trailing spaces, and the + // hard newline must not add an empty line. + { + const std::string wrapped = TrimmingUnitTestWordWrapper("ab cde\nf", 10, FLAG_WRAP_TEXT).Wrapped(); + if (wrapped != "ab \ncde\nf") { + printf("%s: Test Fail (trimming measurer)\n%s\n", __FUNCTION__, wrapped.c_str()); + return false; + } + } + // If there's enough space, it shouldn't wrap. This is exactly enough. EXPECT_WORDWRAP_EQ_STR("Hello", 10, 0, "Hello"); EXPECT_WORDWRAP_EQ_STR("Hello", 10, FLAG_WRAP_TEXT, "Hello"); From 1bce9352fbd843ca059e80fd9966d7e5a30f567a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Henrik=20Rydg=C3=A5rd?= Date: Thu, 5 Nov 2026 21:58:56 +0100 Subject: [PATCH 10/14] Layout tweaks --- Common/UI/TabHolder.cpp | 2 +- UI/GameScreen.cpp | 1 + UI/MainScreen.cpp | 8 ++++---- UI/MiscScreens.cpp | 3 +++ UI/NativeApp.cpp | 3 +++ UI/SettingsPagesDialogScreen.cpp | 4 ++-- UI/SimpleDialogScreen.cpp | 8 ++++---- UI/TabbedDialogScreen.cpp | 2 +- 8 files changed, 19 insertions(+), 12 deletions(-) diff --git a/Common/UI/TabHolder.cpp b/Common/UI/TabHolder.cpp index 0503fdf497..96a536224c 100644 --- a/Common/UI/TabHolder.cpp +++ b/Common/UI/TabHolder.cpp @@ -42,7 +42,7 @@ TabHolder::TabHolder(Orientation orientation, float stripSize, TabHolderFlags fl Add(tabScroll_); } } else { - SetPadding(Margins(10, 0, 0, 0)); + SetPadding(Margins(12, 0, 0, 0)); tabContainer_ = new LinearLayout(ORIENT_VERTICAL, new LayoutParams(stripSize, FILL_PARENT)); tabContainer_->Add(new Spacer(8.0f)); tabContainer_->SetSpacing(0.0f); diff --git a/UI/GameScreen.cpp b/UI/GameScreen.cpp index ba46ea35b3..c4abce42de 100644 --- a/UI/GameScreen.cpp +++ b/UI/GameScreen.cpp @@ -465,6 +465,7 @@ void GameScreen::CreateContentViews(UI::ViewGroup *parent) { auto di = GetI18NCategory(I18NCat::DIALOG); Choice *btnResetTime = timeHoriz->Add(new Choice(di->T("Reset"), new LinearLayoutParams(0.0f, Gravity::G_VCENTER))); + btnResetTime->SetRoundCorners(); btnResetTime->OnClick.Add([this, ga, timeStr, title](UI::EventParams &) { auto di = GetI18NCategory(I18NCat::DIALOG); auto gta = GetI18NCategory(I18NCat::GAME); diff --git a/UI/MainScreen.cpp b/UI/MainScreen.cpp index 4546b094db..507f40a33e 100644 --- a/UI/MainScreen.cpp +++ b/UI/MainScreen.cpp @@ -532,18 +532,18 @@ void MainScreen::CreateViews() { } if (vertical) { - LinearLayout *header = new LinearLayout(ORIENT_HORIZONTAL, new LinearLayoutParams(FILL_PARENT, WRAP_CONTENT, Margins(8, 8, 8, 16))); - header->SetSpacing(5.0f); + LinearLayout *header = new LinearLayout(ORIENT_HORIZONTAL, new LinearLayoutParams(FILL_PARENT, WRAP_CONTENT, Margins(0, 12, 0, 16))); + header->SetSpacing(4.0f); header->Add(new LogoView(true, new LinearLayoutParams(1.0f))); LinearLayout *buttonGroup = new LinearLayout(ORIENT_HORIZONTAL, new LinearLayoutParams(WRAP_CONTENT, WRAP_CONTENT, 1.0f, UI::Gravity::G_VCENTER)); - buttonGroup->SetPadding(Margins(0, 0, 12, 12)); + buttonGroup->SetPadding(Margins(0, 0, 0, 12)); CreateMainButtons(buttonGroup, vertical); header->Add(buttonGroup); - LinearLayout *rootLayout = new LinearLayout(ORIENT_VERTICAL, new LinearLayoutParams(1.0f)); + LinearLayout *rootLayout = new LinearLayout(ORIENT_VERTICAL, new LinearLayoutParams(1.0f, Margins(12, 0, 12, 0))); rootLayout->SetSpacing(0.0f); leftColumn->ReplaceLayoutParams(new LinearLayoutParams(1.0f)); diff --git a/UI/MiscScreens.cpp b/UI/MiscScreens.cpp index c91f302eb0..c5e981a65f 100644 --- a/UI/MiscScreens.cpp +++ b/UI/MiscScreens.cpp @@ -641,6 +641,9 @@ void CreditsScreen::CreateDialogViews(UI::ViewGroup *parent) { System_ShareText(cr->T("CheckOutPPSSPP", "Check out PPSSPP, the awesome PSP emulator: https://www.ppsspp.org/")); }); } + + left->ApplyRoundCorners(); + right->ApplyRoundCorners(); } void CreditsScreen::update() { diff --git a/UI/NativeApp.cpp b/UI/NativeApp.cpp index 5d7b9b927e..845b7f5f74 100644 --- a/UI/NativeApp.cpp +++ b/UI/NativeApp.cpp @@ -910,6 +910,9 @@ void NativeInit(int argc, const char *argv[], const CommandLineOptions &cmdLineO } else if (equals(cmdLineOptions.startScreen.value(), "memsticksetup")) { g_screenManager->switchScreen(new MainScreen()); g_screenManager->push(new MemStickScreen(true)); + } else if (equals(cmdLineOptions.startScreen.value(), "credits")) { + g_screenManager->switchScreen(new MainScreen()); + g_screenManager->push(new CreditsScreen()); } else { WARN_LOG(Log::System, "Unknown start screen '%s'", cmdLineOptions.startScreen.value().c_str()); g_screenManager->switchScreen(new LogoScreen(AfterLogoScreen::DEFAULT)); diff --git a/UI/SettingsPagesDialogScreen.cpp b/UI/SettingsPagesDialogScreen.cpp index 46f5909b2f..ad4bf4488b 100644 --- a/UI/SettingsPagesDialogScreen.cpp +++ b/UI/SettingsPagesDialogScreen.cpp @@ -152,8 +152,8 @@ void SettingsPagesDialogScreen::CreateSettingsTabContents(int settingsTab, UI::L void SettingsPagesDialogScreen::CreateNavBar(UI::ViewGroup *parent, int settingsTab, const std::vector &titles, const std::string &helpTag) { using namespace UI; const SettingsTab &tab = settingsTabs_[settingsTab]; - const Margins barPadding(12, 0, 8, 0); - LinearLayout *bar = parent->Add(new LinearLayout(ORIENT_HORIZONTAL, new LinearLayoutParams(FILL_PARENT, ITEM_HEIGHT, Margins(0, 8)))); + const Margins barPadding(12, 0, 12, 0); + LinearLayout *bar = parent->Add(new LinearLayout(ORIENT_HORIZONTAL, new LinearLayoutParams(FILL_PARENT, ITEM_HEIGHT, Margins(0, 12)))); bar->SetPadding(barPadding); bar->SetSpacing(8.0f); diff --git a/UI/SimpleDialogScreen.cpp b/UI/SimpleDialogScreen.cpp index f831fedc70..5f6154eb20 100644 --- a/UI/SimpleDialogScreen.cpp +++ b/UI/SimpleDialogScreen.cpp @@ -21,7 +21,7 @@ void UISimpleBaseDialogScreen::CreateViews() { topBarFlags |= TopBarFlags::ContextMenuButton; } - TopBar *topBar = root_->Add(new TopBar(*screenManager()->getUIContext(), topBarFlags, GetTitle())); + TopBar *topBar = root_->Add(new TopBar(*screenManager()->getUIContext(), topBarFlags, GetTitle(), new LinearLayoutParams(FILL_PARENT, WRAP_CONTENT, Margins(12, 12, 12, 0)))); if (flags_ & SimpleDialogFlags::CustomContextMenu) { View *menuButton = topBar->GetContextMenuButton(); topBar->OnContextMenuClick.Add([this, menuButton](UI::EventParams &e) { @@ -33,7 +33,7 @@ void UISimpleBaseDialogScreen::CreateViews() { if (flags_ & SimpleDialogFlags::ContentsCanScroll) { ScrollView *scroll = new ScrollView(ORIENT_VERTICAL, new LinearLayoutParams(FILL_PARENT, FILL_PARENT, 1.0f)); - LinearLayout *contents = new LinearLayoutList(ORIENT_VERTICAL, new LinearLayoutParams(Margins(0, 0, portrait ? 0 : 8, 0))); + LinearLayout *contents = new LinearLayoutList(ORIENT_VERTICAL, new LinearLayoutParams(Margins(0, 0, portrait ? 0 : 12, 0))); contents->SetSpacing(0); CreateDialogViews(contents); scroll->Add(contents); @@ -121,13 +121,13 @@ void UITwoPaneBaseDialogScreen::CreateViews() { if (!(flags_ & TwoPaneFlags::SettingsInContextMenu)) { LinearLayout *settingsPane; if (flags_ & TwoPaneFlags::SettingsCanScroll) { - settingsPane = new LinearLayout(ORIENT_VERTICAL, new LinearLayoutParams(FILL_PARENT, WRAP_CONTENT, Margins(8))); + settingsPane = new LinearLayout(ORIENT_VERTICAL, new LinearLayoutParams(FILL_PARENT, WRAP_CONTENT)); settingsPane->SetSpacing(0.0f); ScrollView *settingsScroll = new ScrollView(ORIENT_VERTICAL, new LinearLayoutParams(FILL_PARENT, FILL_PARENT, 1.0f)); settingsScroll->Add(settingsPane); root->Add(settingsScroll); } else { - settingsPane = new LinearLayout(ORIENT_VERTICAL, new LinearLayoutParams(FILL_PARENT, WRAP_CONTENT, 0.0f, Margins(8))); + settingsPane = new LinearLayout(ORIENT_VERTICAL, new LinearLayoutParams(FILL_PARENT, WRAP_CONTENT, 0.0f)); settingsPane->SetSpacing(0.0f); root->Add(settingsPane); } diff --git a/UI/TabbedDialogScreen.cpp b/UI/TabbedDialogScreen.cpp index e0cd34b111..85ebbcec4f 100644 --- a/UI/TabbedDialogScreen.cpp +++ b/UI/TabbedDialogScreen.cpp @@ -28,7 +28,7 @@ void UITabbedBaseDialogScreen::AddTab(const char *tag, std::string_view title, I scroll = new ScrollView(ORIENT_VERTICAL, new LinearLayoutParams(FILL_PARENT, FILL_PARENT)); scroll->SetTag(tag); } - LinearLayout *contents = new LinearLayoutList(ORIENT_VERTICAL, new LinearLayoutParams(Margins(0, 0, 8, 0))); + LinearLayout *contents = new LinearLayoutList(ORIENT_VERTICAL, new LinearLayoutParams(Margins(0, 0, 12, 0))); contents->SetSpacing(0); if (dialogFlags & TabDialogFlags::AddAutoTitles) { From d7d42ad95a83feaef745a5ab8abbbdcf7d892e32 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Henrik=20Rydg=C3=A5rd?= Date: Thu, 5 Nov 2026 22:03:28 +0100 Subject: [PATCH 11/14] UI: Keep squeezed text from wrapping due to float rounding The squeeze scale is availWidth / width, and multiplying the width back by it could land a hair above availWidth (189.00002 for "PPSSPP Forums" on the credits screen), so the text got wrapped despite being scaled to fit. Shave off 0.1%. Co-Authored-By: Claude Opus 5.5 --- Common/UI/Context.cpp | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/Common/UI/Context.cpp b/Common/UI/Context.cpp index 118d1ac1bc..a9c2169780 100644 --- a/Common/UI/Context.cpp +++ b/Common/UI/Context.cpp @@ -354,7 +354,9 @@ float UIContext::CalculateTextScale(std::string_view str, float availWidth) cons MeasureTextRect(theme->uiFont, 1.0f, 1.0f, str, availWidth, &actualWidth, &actualHeight, ALIGN_VCENTER); if (actualWidth > availWidth) { // TODO: Return feedback that wrapping was needed. - return std::max(MIN_TEXT_SCALE, availWidth / actualWidth); + // Shave off a little, since float rounding can otherwise leave the scaled text a hair + // wider than availWidth, and then it gets wrapped anyway. + return std::max(MIN_TEXT_SCALE, availWidth / actualWidth * 0.999f); } return 1.0f; } From 6c624dbdda3e04d08a62a6ae1581e8c9aae329e2 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Henrik=20Rydg=C3=A5rd?= Date: Thu, 5 Nov 2026 22:10:48 +0100 Subject: [PATCH 12/14] Fix displayed icons for homebrew without icons --- UI/GameButton.cpp | 5 +---- 1 file changed, 1 insertion(+), 4 deletions(-) diff --git a/UI/GameButton.cpp b/UI/GameButton.cpp index b85be43515..6c33ff09a8 100644 --- a/UI/GameButton.cpp +++ b/UI/GameButton.cpp @@ -82,6 +82,7 @@ static ImageID ChooseGameIcon(const std::shared_ptr &ginfo) { // The XMB is an ELF too, but has an icon. return ImageID("I_APP"); case IdentifiedFileType::UNKNOWN_ELF: return ImageID("I_APP"); break; + case IdentifiedFileType::PSP_PBP_DIRECTORY: return ImageID("I_APP"); break; case IdentifiedFileType::PPSSPP_GE_DUMP: return ImageID("I_DISPLAY"); break; case IdentifiedFileType::PSX_ISO: case IdentifiedFileType::PSP_PS1_PBP: return ImageID("I_PSX_ISO"); break; @@ -176,9 +177,6 @@ void GameButton::Draw(UIContext &dc) { texture = ginfo->icon.texture; // The type icon is only for when there's no picture, and would be drawn on top of it. imageIcon = ImageID::invalid(); - } else { - // No icon, but drawBackground is set. Let's show a plain icon depending on type. - imageIcon = ChooseGameIcon(ginfo); } } else if (gamePath_.Type() == PathType::HTTP) { // Make sure that pending HTTP icons show something, instead of just blank. @@ -290,7 +288,6 @@ void GameButton::Draw(UIContext &dc) { dc.Draw()->Flush(); } - if (viewMode_ == GameBrowserViewMode::GRID) { if (g_Config.bShowIDOnGameIcon && fileType != IdentifiedFileType::PSP_ELF && !id_version.empty()) { std::string_view idStr = id_version; From c3c6327294084223d90dba627d336bcf2d7843f2 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Henrik=20Rydg=C3=A5rd?= Date: Thu, 5 Nov 2026 22:24:29 +0100 Subject: [PATCH 13/14] More layout tweaks --- UI/ControlMappingScreen.cpp | 6 +++--- UI/CustomButtonMappingScreen.cpp | 7 +++++-- UI/SimpleDialogScreen.cpp | 6 +++--- 3 files changed, 11 insertions(+), 8 deletions(-) diff --git a/UI/ControlMappingScreen.cpp b/UI/ControlMappingScreen.cpp index 05ec47eb09..dfa2f22766 100644 --- a/UI/ControlMappingScreen.cpp +++ b/UI/ControlMappingScreen.cpp @@ -104,6 +104,7 @@ void SingleControlMapper::Refresh() { float rightColumnWidth = 350; LinearLayout *root = Add(new LinearLayout(ORIENT_HORIZONTAL, new LinearLayoutParams(FILL_PARENT, WRAP_CONTENT))); + root->SetPadding(Margins(0, 0, 12, 0)); root->SetSpacing(3.0f); auto iter = keyImages.find(keyName_); @@ -124,7 +125,7 @@ void SingleControlMapper::Refresh() { p->OnClick.Handle(this, &SingleControlMapper::OnAddMouse); } - LinearLayout *rightColumn = root->Add(new LinearLayout(ORIENT_VERTICAL, portrait_ ? new LinearLayoutParams(1.0f) : new LinearLayoutParams(rightColumnWidth, WRAP_CONTENT))); + LinearLayout *rightColumn = root->Add(new LinearLayout(ORIENT_VERTICAL, new LinearLayoutParams(1.0f))); rightColumn->SetSpacing(2.0f); std::vector mappings; KeyMap::InputMappingsFromPspButton(pspKey_, &mappings, false); @@ -234,8 +235,6 @@ void ControlMappingScreen::CreateSettingsViews(UI::ViewGroup *parent) { // Use the whole width of the narrow settings column. ChoiceGroupList *groups = parent->Add(new ChoiceGroupList()); - groups->SetPadding(Margins(0, 12, 0, 0)); - groups->SetSpacing(12.0f); ChoiceGroup *resetChoices = groups->Add(new ChoiceGroup()); resetChoices->Add(new Choice(km->T("Clear All")))->OnClick.Add([](UI::EventParams &) { @@ -268,6 +267,7 @@ void ControlMappingScreen::CreateContentViews(UI::ViewGroup *parent) { using namespace UI; LinearLayout *rootLayout = parent->Add(new LinearLayout(ORIENT_VERTICAL, new LinearLayoutParams(FILL_PARENT, WRAP_CONTENT))); + rootLayout->SetPadding(Margins(0, 12, 12, 12)); mappers_.clear(); size_t numMappableKeys = 0; diff --git a/UI/CustomButtonMappingScreen.cpp b/UI/CustomButtonMappingScreen.cpp index 8813f25416..22e4130304 100644 --- a/UI/CustomButtonMappingScreen.cpp +++ b/UI/CustomButtonMappingScreen.cpp @@ -104,8 +104,11 @@ public: uint32_t colorBg = colorAlpha(g_Config.iTouchButtonStyle != 0 ? 0xFFFFFF : 0xc0b080, opacity); uint32_t color = colorAlpha(0xFFFFFF, opacity); - dc.Draw()->DrawImageRotated(bgImg_, x_, y_, 1.0f, rotS_*PI/180, colorBg, flipS_); - dc.Draw()->DrawImageRotated(img_, x_, y_, 1.0f, rotI_*PI/180, color, false); + const float x = bounds_.x + x_; + const float y = bounds_.y + y_; + + dc.Draw()->DrawImageRotated(bgImg_, x, y, 1.0f, rotS_*PI/180, colorBg, flipS_); + dc.Draw()->DrawImageRotated(img_, x, y, 1.0f, rotI_*PI/180, color, false); } private: int x_; diff --git a/UI/SimpleDialogScreen.cpp b/UI/SimpleDialogScreen.cpp index 5f6154eb20..80cac2621c 100644 --- a/UI/SimpleDialogScreen.cpp +++ b/UI/SimpleDialogScreen.cpp @@ -146,7 +146,7 @@ void UITwoPaneBaseDialogScreen::CreateViews() { } TopBar *topBar = nullptr; if (!(flags_ & TwoPaneFlags::NoTopbarInLandscape)) { - topBar = root->Add(new TopBar(*screenManager()->getUIContext(), topBarFlags, title)); + topBar = root->Add(new TopBar(*screenManager()->getUIContext(), topBarFlags, title, new LinearLayoutParams(FILL_PARENT, WRAP_CONTENT, Margins(12, 12, 12, 12)))); } root->SetSpacing(0); @@ -162,13 +162,13 @@ void UITwoPaneBaseDialogScreen::CreateViews() { root->Add(columns); // root_->Add(new TopBar(*screenManager()->getUIContext(), portrait, GetTitle(), new LayoutParams(FILL_PARENT, FILL_PARENT))); - ScrollView *settingsScroll = new ScrollView(ORIENT_VERTICAL, new LinearLayoutParams(SettingsWidth(), FILL_PARENT, 0.0f, Margins(0, 8, 0, 0))); + ScrollView *settingsScroll = new ScrollView(ORIENT_VERTICAL, new LinearLayoutParams(SettingsWidth(), FILL_PARENT, 0.0f, Margins(0, 0, 0, 0))); LinearLayout *settingsPane = new LinearLayout(ORIENT_VERTICAL, new LinearLayoutParams(FILL_PARENT, Margins(0, 8, 0, 0))); settingsPane->SetSpacing(0.0f); if (flags_ & TwoPaneFlags::NoTopbarInLandscape) { // We still need a back button. - Choice *backButton = settingsPane->Add(new Choice(di->T("Back"), ImageID("I_NAVIGATE_BACK"))); + Choice *backButton = settingsPane->Add(new Choice(di->T("Back"), ImageID("I_NAVIGATE_BACK"), new LinearLayoutParams(FILL_PARENT, WRAP_CONTENT, Margins(12, 0, 12, 0)))); backButton->OnClick.Handle(this, &UIScreen::OnBack); backButton->SetRoundCorners(); settingsPane->Add(new Spacer(8.0f)); From fa395a4f31a12b993d2d571a942c22946f18126b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Henrik=20Rydg=C3=A5rd?= Date: Thu, 5 Nov 2026 22:39:56 +0100 Subject: [PATCH 14/14] PauseScreen layout tweaks --- UI/PauseScreen.cpp | 11 ++++++----- 1 file changed, 6 insertions(+), 5 deletions(-) diff --git a/UI/PauseScreen.cpp b/UI/PauseScreen.cpp index 0e5b6f3ae9..bf346ae2d8 100644 --- a/UI/PauseScreen.cpp +++ b/UI/PauseScreen.cpp @@ -495,7 +495,7 @@ void GamePauseScreen::CreateSavestateControls(UI::LinearLayout *leftColumnItems, *extraRow = nullptr; - LinearLayout *buttonRow = new LinearLayout(ORIENT_HORIZONTAL, new LinearLayoutParams(Margins(10, 0, 0, 10))); + LinearLayout *buttonRow = new LinearLayout(ORIENT_HORIZONTAL, new LinearLayoutParams(Margins(0, 0, 0, 0))); if (g_Config.bEnableStateUndo && !Achievements::HardcoreModeActive() && NetworkAllowSaveState()) { UI::Button *saveUndoButton = buttonRow->Add(new Button(pa->T("Undo last save"), ImageID("I_NAVIGATE_BACK"), new LinearLayoutParams(WRAP_CONTENT, ITEM_HEIGHT))); saveUndoButton->SetEnabled(SaveState::HasUndoLastSave(saveStatePrefix_)); @@ -544,9 +544,9 @@ UI::Margins GamePauseScreen::RootMargins() const { void GamePauseScreen::CreateViews() { using namespace UI; - bool portrait = GetDeviceOrientation() == DeviceOrientation::Portrait; + const bool portrait = GetDeviceOrientation() == DeviceOrientation::Portrait; - Margins scrollMargins(0, 10, 0, 0); + Margins scrollMargins(12, 10, 12, 0); Margins actionMenuMargins(0, 10, 15, 0); auto gr = GetI18NCategory(I18NCat::GRAPHICS); auto pa = GetI18NCategory(I18NCat::PAUSE); @@ -571,7 +571,7 @@ void GamePauseScreen::CreateViews() { } else { title = g_paramSFO.GetValueString("TITLE"); } - TopBar *topBar = new TopBar(*screenManager()->getUIContext(), TopBarFlags::ContextMenuButton, title); + TopBar *topBar = new TopBar(*screenManager()->getUIContext(), TopBarFlags::ContextMenuButton, title, new LinearLayoutParams(FILL_PARENT, WRAP_CONTENT, Margins(12, 12, 12, 0))); root_->Add(topBar); topBar->OnContextMenuClick.Add([this, portrait](UI::EventParams &e) { @@ -724,7 +724,7 @@ void GamePauseScreen::CreateViews() { root_->Add(buttonColumnScroll); } - LinearLayout *rightColumnItems = new LinearLayout(ORIENT_VERTICAL); + LinearLayout *rightColumnItems = new ChoiceGroupList(); rightColumnItems->SetSpacing(12.0f); buttonColumn->Add(rightColumnItems); @@ -854,6 +854,7 @@ void GamePauseScreen::CreateViews() { } if (!portrait) { + middleColumn->Add(new Spacer(12.0f)); Choice *menuButton = middleColumn->Add(new Choice("", ImageID("I_THREE_DOTS"), new LinearLayoutParams(64, 64))); menuButton->OnClick.Add([this, menuButton, portrait](UI::EventParams &e) { ShowContextMenu(menuButton, portrait);