From 0066599f9a122ee082991e7145514fa62c84c322 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Henrik=20Rydg=C3=A5rd?= Date: Wed, 14 Oct 2026 10:24:32 +0200 Subject: [PATCH] Game browser: Add an XMB-style large list mode - Fixes #22202 A third view mode next to grid and list: a list with full size icons, scaled by the grid icon size setting. Like the XMB, the selected (or hovered) game plays its ICON1.PMF in place of the icon. The per-tab grid bools become GameBrowserView1-4 ints. The old GridView keys are still read, to pick the default, but no longer saved. The PMF decoding moved out of GameScreen's PMFView into a shared PMFVideoTexture. Co-Authored-By: Claude Opus 5.5 (1M context) --- Core/Config.cpp | 22 +++++- Core/Config.h | 7 +- Core/ConfigValues.h | 6 ++ UI/GameBrowser.cpp | 153 +++++++++++++++++++++++++++++------- UI/GameBrowser.h | 8 +- UI/GameScreen.cpp | 71 ++--------------- UI/MainScreen.cpp | 24 +++--- UI/MainScreen.h | 2 +- UI/MiscViews.cpp | 52 ++++++++++++ UI/MiscViews.h | 29 +++++++ UI/RemoteISOScreen.cpp | 2 +- UI/UIAtlas.cpp | 1 + assets/ui_images/images.svg | 19 ++++- 13 files changed, 279 insertions(+), 117 deletions(-) diff --git a/Core/Config.cpp b/Core/Config.cpp index fde77e6207..17dd44d34a 100644 --- a/Core/Config.cpp +++ b/Core/Config.cpp @@ -195,6 +195,15 @@ static bool DefaultEnableStateUndo() { return true; } +static int GameBrowserViewFromGridView(bool gridView) { + return (int)(gridView ? GameBrowserViewMode::GRID : GameBrowserViewMode::LIST); +} + +static int DefaultGameBrowserView1() { return GameBrowserViewFromGridView(g_Config.bGridView1); } +static int DefaultGameBrowserView2() { return GameBrowserViewFromGridView(g_Config.bGridView2); } +static int DefaultGameBrowserView3() { return GameBrowserViewFromGridView(g_Config.bGridView3); } +static int DefaultGameBrowserView4() { return GameBrowserViewFromGridView(g_Config.bGridView4); } + static float DefaultGameGridScale() { #if PPSSPP_PLATFORM(IOS) return 1.25f; @@ -283,10 +292,15 @@ static const ConfigSetting generalSettings[] = { ConfigSetting("ShowRegionOnGameIcon", SETTING(g_Config, bShowRegionOnGameIcon), false, CfgFlag::DEFAULT), ConfigSetting("ShowIDOnGameIcon", SETTING(g_Config, bShowIDOnGameIcon), false, CfgFlag::DEFAULT), ConfigSetting("GameGridScale", SETTING(g_Config, fGameGridScale), &DefaultGameGridScale, CfgFlag::DEFAULT), - ConfigSetting("GridView1", SETTING(g_Config, bGridView1), true, CfgFlag::DEFAULT), - ConfigSetting("GridView2", SETTING(g_Config, bGridView2), true, CfgFlag::DEFAULT), - ConfigSetting("GridView3", SETTING(g_Config, bGridView3), false, CfgFlag::DEFAULT), - ConfigSetting("GridView4", SETTING(g_Config, bGridView4), false, CfgFlag::DEFAULT), + ConfigSetting("GridView1", SETTING(g_Config, bGridView1), true, CfgFlag::DONT_SAVE), + ConfigSetting("GridView2", SETTING(g_Config, bGridView2), true, CfgFlag::DONT_SAVE), + ConfigSetting("GridView3", SETTING(g_Config, bGridView3), false, CfgFlag::DONT_SAVE), + ConfigSetting("GridView4", SETTING(g_Config, bGridView4), false, CfgFlag::DONT_SAVE), + // These must come after GridView*, which their defaults are taken from. + ConfigSetting("GameBrowserView1", SETTING(g_Config, iGameBrowserView1), &DefaultGameBrowserView1, CfgFlag::DEFAULT), + ConfigSetting("GameBrowserView2", SETTING(g_Config, iGameBrowserView2), &DefaultGameBrowserView2, CfgFlag::DEFAULT), + ConfigSetting("GameBrowserView3", SETTING(g_Config, iGameBrowserView3), &DefaultGameBrowserView3, CfgFlag::DEFAULT), + ConfigSetting("GameBrowserView4", SETTING(g_Config, iGameBrowserView4), &DefaultGameBrowserView4, CfgFlag::DEFAULT), ConfigSetting("RightAnalogUp", SETTING(g_Config, iRightAnalogUp), 0, CfgFlag::PER_GAME), ConfigSetting("RightAnalogDown", SETTING(g_Config, iRightAnalogDown), 0, CfgFlag::PER_GAME), ConfigSetting("RightAnalogLeft", SETTING(g_Config, iRightAnalogLeft), 0, CfgFlag::PER_GAME), diff --git a/Core/Config.h b/Core/Config.h index 6fa8be43ea..d472373c15 100644 --- a/Core/Config.h +++ b/Core/Config.h @@ -468,7 +468,12 @@ public: int iTiltInputType; bool bTiltInputEnabled; - // The four tabs (including Remote last) + // The four tabs (including Remote last). Values from GameBrowserViewMode. + int iGameBrowserView1; + int iGameBrowserView2; + int iGameBrowserView3; + int iGameBrowserView4; + // Only read, to migrate to iGameBrowserView*. bool bGridView1; bool bGridView2; bool bGridView3; diff --git a/Core/ConfigValues.h b/Core/ConfigValues.h index 3fd223cf02..9a5428c361 100644 --- a/Core/ConfigValues.h +++ b/Core/ConfigValues.h @@ -302,3 +302,9 @@ enum class RemoteISOShareType : int { RECENT, LOCAL_FOLDER, }; + +enum class GameBrowserViewMode : int { + GRID = 0, + LIST = 1, + LARGE_LIST = 2, // XMB-style, with full size icons and the ICON1.PMF video. +}; diff --git a/UI/GameBrowser.cpp b/UI/GameBrowser.cpp index bc394e0028..e5df693610 100644 --- a/UI/GameBrowser.cpp +++ b/UI/GameBrowser.cpp @@ -38,6 +38,7 @@ #include "Core/Util/RecentFiles.h" #include "Core/HLE/sceCtrl.h" #include "Core/ELF/PBPReader.h" +#include "Core/Util/VideoPlayer.h" #include "UI/GameBrowser.h" #include "UI/RemoteISOScreen.h" @@ -49,7 +50,21 @@ #include "Common/Data/Text/I18n.h" #include "Core/Util/DarwinFileSystemServices.h" // For the browser -static void DrawIconWithText(UIContext &dc, ImageID image, std::string_view text, const Bounds &bounds, bool gridStyle, const UI::Style &style) { +// Icon dimensions in the large list, and its padding around the icon. +static constexpr float LARGE_LIST_ICON_W = 144.0f; +static constexpr float LARGE_LIST_ICON_H = 80.0f; +static constexpr float LARGE_LIST_PAD = 6.0f; + +// Where the text starts in the list modes. +static float ListTextX(GameBrowserViewMode viewMode) { + if (viewMode == GameBrowserViewMode::LARGE_LIST) { + return LARGE_LIST_PAD + LARGE_LIST_ICON_W * g_Config.fGameGridScale + 20.0f; + } + return 150.0f; +} + +static void DrawIconWithText(UIContext &dc, ImageID image, std::string_view text, const Bounds &bounds, GameBrowserViewMode viewMode, const UI::Style &style) { + const bool gridStyle = viewMode == GameBrowserViewMode::GRID; float tw, th; dc.MeasureText(dc.GetFontStyle(), gridStyle ? g_Config.fGameGridScale : 1.0, gridStyle ? g_Config.fGameGridScale : 1.0, text, &tw, &th, 0); @@ -82,15 +97,21 @@ static void DrawIconWithText(UIContext &dc, ImageID image, std::string_view text dc.SetFontStyle(dc.GetTheme().uiFont); dc.PopScissor(); } else { - dc.Draw()->DrawImage(image, bounds.x + 72, bounds.centerY(), 0.88f * (gridStyle ? g_Config.fGameGridScale : 1.0), style.fgColor, ALIGN_CENTER); + const float textX = ListTextX(viewMode); + if (viewMode == GameBrowserViewMode::LARGE_LIST) { + const float scale = g_Config.fGameGridScale; + dc.Draw()->DrawImage(image, bounds.x + LARGE_LIST_PAD + LARGE_LIST_ICON_W * 0.5f * scale, bounds.centerY(), 0.88f * scale, style.fgColor, ALIGN_CENTER); + } else { + dc.Draw()->DrawImage(image, bounds.x + 72, bounds.centerY(), 0.88f * (gridStyle ? g_Config.fGameGridScale : 1.0), style.fgColor, ALIGN_CENTER); + } - float tx = 150; - int availableWidth = bounds.w - 150; + float tx = textX; + int availableWidth = bounds.w - textX; float sineWidth = std::max(0.0f, (tw - availableWidth)) / 2.0f; if (availableWidth < tw) { tx -= (1.0f + sin(time_now_d() * 1.5f)) * sineWidth; Bounds tb = bounds; - tb.x = bounds.x + 150; + tb.x = bounds.x + textX; tb.w = availableWidth; dc.PushScissor(tb); } @@ -103,8 +124,8 @@ static void DrawIconWithText(UIContext &dc, ImageID image, std::string_view text class GameButton : public UI::Clickable { public: - GameButton(const Path &gamePath, bool gridStyle, UI::LayoutParams *layoutParams = nullptr) - : UI::Clickable(layoutParams), gridStyle_(gridStyle), gamePath_(gamePath) {} + GameButton(const Path &gamePath, GameBrowserViewMode viewMode, UI::LayoutParams *layoutParams = nullptr) + : UI::Clickable(layoutParams), viewMode_(viewMode), gridStyle_(viewMode == GameBrowserViewMode::GRID), gamePath_(gamePath) {} void Draw(UIContext &dc) override; std::string DescribeText() const override; @@ -122,6 +143,9 @@ public: if (gridStyle_) { w = 144 * g_Config.fGameGridScale; h = 80 * g_Config.fGameGridScale; + } else if (viewMode_ == GameBrowserViewMode::LARGE_LIST) { + w = 500; + h = LARGE_LIST_ICON_H * g_Config.fGameGridScale + LARGE_LIST_PAD * 2; } else { w = 500; h = 50; @@ -188,6 +212,12 @@ public: TriggerOnHighlight(focusFlags); } + void DeviceLost() override { + if (video_) { + video_->DeviceLost(); + } + } + UI::Event OnHoldClick; UI::Event OnHighlight; @@ -208,10 +238,17 @@ private: OnHighlight.Trigger(e); } + void UpdateVideo(const std::shared_ptr &ginfo, bool wantVideo); + + GameBrowserViewMode viewMode_; bool gridStyle_; Path gamePath_; std::string title_; + // The ICON1.PMF video, played in place of the icon in the large list, like the XMB does. + std::unique_ptr video_; + Instant videoStart_; + double holdStart_ = 0.0; bool holdEnabled_ = true; bool showInfoPressed_ = false; @@ -219,8 +256,36 @@ private: bool hovering_ = false; }; +void GameButton::UpdateVideo(const std::shared_ptr &ginfo, bool wantVideo) { + if (!wantVideo) { + video_.reset(); + return; + } + if (video_ || !ginfo->Ready(GameInfoFlags::ICON1_PMF)) { + return; + } + std::string data; + { + std::lock_guard guard(ginfo->lock); + data = ginfo->icon1pmf; + } + if (!data.empty()) { + video_ = std::make_unique(std::move(data)); + videoStart_ = Instant::Now(); + } +} + void GameButton::Draw(UIContext &dc) { - std::shared_ptr ginfo = g_gameInfoCache->GetInfo(dc.GetDrawContext(), gamePath_, GameInfoFlags::PARAM_SFO | GameInfoFlags::ICON); + const bool largeList = viewMode_ == GameBrowserViewMode::LARGE_LIST; + // Like the XMB, only the selected game plays its video. The mouse can select one too, by hovering. + const bool wantVideo = largeList && (HasFocus() || hovering_) && pmf_player_available(); + + GameInfoFlags infoFlags = GameInfoFlags::PARAM_SFO | GameInfoFlags::ICON; + if (wantVideo) { + infoFlags |= GameInfoFlags::ICON1_PMF; + } + std::shared_ptr ginfo = g_gameInfoCache->GetInfo(dc.GetDrawContext(), gamePath_, infoFlags); + UpdateVideo(ginfo, wantVideo); Draw::Texture *texture = nullptr; u32 color = 0, shadowColor = 0; using namespace UI; @@ -293,6 +358,13 @@ void GameButton::Draw(UIContext &dc) { int y = bounds_.y; int w = gridStyle_ ? bounds_.w : 144; int h = bounds_.h; + if (largeList) { + x += LARGE_LIST_PAD; + y += LARGE_LIST_PAD; + w = LARGE_LIST_ICON_W * g_Config.fGameGridScale; + h = LARGE_LIST_ICON_H * g_Config.fGameGridScale; + } + const float textX = ListTextX(viewMode_); if (!gridStyle_ || !texture) { if (HasFocus()) @@ -306,6 +378,13 @@ void GameButton::Draw(UIContext &dc) { dc.Draw()->Flush(); } + if (texture && video_) { + Draw::Texture *frame = video_->Update(dc.GetDrawContext(), videoStart_.ElapsedSeconds()); + if (frame) { + texture = frame; + } + } + if (texture) { color = whiteAlpha(ease((time_now_d() - ginfo->icon.timeLoaded) * 2)); shadowColor = blackAlpha(ease((time_now_d() - ginfo->icon.timeLoaded) * 2)); @@ -325,7 +404,7 @@ void GameButton::Draw(UIContext &dc) { // Render button int dropsize = 10; if (texture) { - if (!gridStyle_) { + if (!gridStyle_ && !largeList) { x += 4; } if (txOffset) { @@ -371,7 +450,7 @@ void GameButton::Draw(UIContext &dc) { if (down_) style = dc.GetTheme().itemDownStyle; if (!IsEnabled()) style = dc.GetTheme().itemDisabledStyle; - DrawIconWithText(dc, imageIcon, ginfo->GetTitle(), bounds_, gridStyle_, style); + DrawIconWithText(dc, imageIcon, ginfo->GetTitle(), bounds_, viewMode_, style); if (overlayColor) { dc.FillRect(Drawable(overlayColor), overlayBounds); @@ -397,22 +476,22 @@ void GameButton::Draw(UIContext &dc) { dc.MeasureText(dc.GetFontStyle(), 1.0f, 1.0f, title_, &tw, &th, 0); - int availableWidth = bounds_.w - 150; + int availableWidth = bounds_.w - textX; if (g_Config.bShowIDOnGameIcon) { float vw, vh; dc.MeasureText(dc.GetFontStyle(), 0.7f, 0.7f, ginfo->id_version, &vw, &vh, 0); availableWidth -= vw + 20; dc.SetFontScale(0.7f, 0.7f); - dc.DrawText(ginfo->id_version, bounds_.x + availableWidth + 160, bounds_.centerY(), style.fgColor, ALIGN_VCENTER); + dc.DrawText(ginfo->id_version, bounds_.x + availableWidth + textX + 10, bounds_.centerY(), style.fgColor, ALIGN_VCENTER); dc.SetFontScale(1.0f, 1.0f); } float sineWidth = std::max(0.0f, (tw - availableWidth)) / 2.0f; - float tx = 150; + float tx = textX; if (availableWidth < tw) { tx -= (1.0f + sin(time_now_d() * 1.5f)) * sineWidth; Bounds tb = bounds_; - tb.x = bounds_.x + 150; + tb.x = bounds_.x + textX; tb.w = availableWidth; dc.PushScissor(tb); } @@ -438,6 +517,8 @@ void GameButton::Draw(UIContext &dc) { if (gearImage) { if (gridStyle_) { dc.Draw()->DrawImage(ImageID("I_GEAR_SMALL"), bounds_.x, y + h - gearImage->h * g_Config.fGameGridScale, g_Config.fGameGridScale); + } else if (largeList) { + dc.Draw()->DrawImage(ImageID("I_GEAR_SMALL"), bounds_.x + LARGE_LIST_PAD, y + h - gearImage->h * g_Config.fGameGridScale, g_Config.fGameGridScale); } else { dc.Draw()->DrawImage(ImageID("I_GEAR_SMALL"), bounds_.x + 4, y, 1.0f); } @@ -459,6 +540,9 @@ void GameButton::Draw(UIContext &dc) { if (gridStyle_) { dc.Draw()->DrawImage(regionIcons[regionIndex], bounds_.x + bounds_.w - (image->w + 5) * g_Config.fGameGridScale, y + h - (image->h + 5) * g_Config.fGameGridScale, g_Config.fGameGridScale); + } else if (largeList) { + dc.Draw()->DrawImage(regionIcons[regionIndex], bounds_.x + LARGE_LIST_PAD + (LARGE_LIST_ICON_W - image->w - 5) * g_Config.fGameGridScale, + y + h - (image->h + 5) * g_Config.fGameGridScale, g_Config.fGameGridScale); } else { dc.Draw()->DrawImage(regionIcons[regionIndex], bounds_.x + 4, y + h - image->h - 5, 1.0f); } @@ -530,11 +614,11 @@ void DirButton::Draw(UIContext &dc) { if (!IsEnabled()) style = dc.GetTheme().itemDisabledStyle; dc.FillRect(style.background, bounds_); - DrawIconWithText(dc, image, text, bounds_, gridStyle_, style); + DrawIconWithText(dc, image, text, bounds_, gridStyle_ ? GameBrowserViewMode::GRID : GameBrowserViewMode::LIST, style); } -GameBrowser::GameBrowser(int token, const Path &path, BrowseFlags browseFlags, bool portrait, bool *gridStyle, ScreenManager *screenManager, std::string_view lastText, std::string_view lastLink, UI::LayoutParams *layoutParams) - : LinearLayout(ORIENT_VERTICAL, layoutParams), gridStyle_(gridStyle), browseFlags_(browseFlags), portrait_(portrait), lastText_(lastText), lastLink_(lastLink), screenManager_(screenManager), token_(token) { +GameBrowser::GameBrowser(int token, const Path &path, BrowseFlags browseFlags, bool portrait, int *viewMode, ScreenManager *screenManager, std::string_view lastText, std::string_view lastLink, UI::LayoutParams *layoutParams) + : LinearLayout(ORIENT_VERTICAL, layoutParams), viewMode_(viewMode), browseFlags_(browseFlags), portrait_(portrait), lastText_(lastText), lastLink_(lastLink), screenManager_(screenManager), token_(token) { using namespace UI; path_.SetUserAgent(StringFromFormat("PPSSPP/%s", PPSSPP_GIT_VERSION)); Path memstickRoot = GetSysDirectory(DIRECTORY_MEMSTICK_ROOT); @@ -580,8 +664,15 @@ void GameBrowser::SetSearchFilter(const std::string &filter, bool setKeyboardFoc search_.ApplySearchFilter(gameList_, setKeyboardFocus); } +GameBrowserViewMode GameBrowser::ViewMode() const { + if (*viewMode_ < (int)GameBrowserViewMode::GRID || *viewMode_ > (int)GameBrowserViewMode::LARGE_LIST) { + return GameBrowserViewMode::GRID; + } + return (GameBrowserViewMode)*viewMode_; +} + void GameBrowser::LayoutChange(UI::EventParams &e) { - *gridStyle_ = e.a == 0 ? true : false; + *viewMode_ = e.a; refreshPending_ = true; } @@ -683,7 +774,7 @@ void GameBrowser::Update() { void GameBrowser::Draw(UIContext &dc) { using namespace UI; - if (lastScale_ != g_Config.fGameGridScale || lastLayoutWasGrid_ != *gridStyle_) { + if (lastScale_ != g_Config.fGameGridScale || lastViewMode_ != ViewMode()) { refreshPending_ = true; } @@ -747,7 +838,8 @@ void GameBrowser::Refresh() { using namespace UI; lastScale_ = g_Config.fGameGridScale; - lastLayoutWasGrid_ = *gridStyle_; + lastViewMode_ = ViewMode(); + const bool gridStyle = lastViewMode_ == GameBrowserViewMode::GRID; // Kill all the contents Clear(); @@ -822,7 +914,8 @@ void GameBrowser::Refresh() { ChoiceStrip *layoutChoice = topBar->Add(new ChoiceStrip(ORIENT_HORIZONTAL)); layoutChoice->AddChoice(ImageID("I_GRID")); layoutChoice->AddChoice(ImageID("I_LINES")); - layoutChoice->SetSelection(*gridStyle_ ? 0 : 1, false); + layoutChoice->AddChoice(ImageID("I_LARGE_LIST")); + layoutChoice->SetSelection((int)lastViewMode_, false); layoutChoice->OnChoice.Handle(this, &GameBrowser::LayoutChange); topBar->Add(new Choice(ImageID("I_ROTATE_LEFT"), new LayoutParams(64.0f, 64.0f)))->OnClick.Add([=](UI::EventParams &e) { path_.Refresh(); @@ -830,7 +923,7 @@ void GameBrowser::Refresh() { }); topBar->Add(new Choice(ImageID("I_GEAR"), new LayoutParams(64.0f, 64.0f)))->OnClick.Handle(this, &GameBrowser::GridSettingsClick); - if (*gridStyle_) { + if (gridStyle) { gameList_ = new UI::GridLayoutList(UI::GridLayoutSettings(150 * g_Config.fGameGridScale, 85 * g_Config.fGameGridScale), new LinearLayoutParams(FILL_PARENT, WRAP_CONTENT, Margins(10, 0, 6, 0))); } else { UI::LinearLayout *gl = new UI::LinearLayoutList(ORIENT_VERTICAL, new LinearLayoutParams(FILL_PARENT, WRAP_CONTENT)); @@ -838,7 +931,7 @@ void GameBrowser::Refresh() { gameList_ = gl; } } else { - if (*gridStyle_) { + if (gridStyle) { gameList_ = new UI::GridLayoutList(UI::GridLayoutSettings(150 * g_Config.fGameGridScale, 85 * g_Config.fGameGridScale), new LinearLayoutParams(FILL_PARENT, WRAP_CONTENT, Margins(10, 0, 6, 0))); } else { UI::LinearLayout *gl = new UI::LinearLayout(ORIENT_VERTICAL, new LinearLayoutParams(FILL_PARENT, WRAP_CONTENT)); @@ -870,7 +963,7 @@ void GameBrowser::Refresh() { std::vector filenames; if (HasSpecialFiles(filenames)) { for (size_t i = 0; i < filenames.size(); i++) { - gameButtons.push_back(new GameButton(filenames[i], *gridStyle_, new UI::LinearLayoutParams(*gridStyle_ == true ? UI::WRAP_CONTENT : UI::FILL_PARENT, UI::WRAP_CONTENT))); + gameButtons.push_back(new GameButton(filenames[i], lastViewMode_, new UI::LinearLayoutParams(gridStyle ? UI::WRAP_CONTENT : UI::FILL_PARENT, UI::WRAP_CONTENT))); } } else if (!listingPending_) { std::vector fileInfo; @@ -888,10 +981,10 @@ void GameBrowser::Refresh() { if (!isGame && !isSaveData) { if (browseFlags_ & BrowseFlags::NAVIGATE) { - dirButtons.push_back(new DirButton(fileInfo[i].fullName, fileInfo[i].name, *gridStyle_, new UI::LinearLayoutParams(UI::FILL_PARENT, UI::FILL_PARENT))); + dirButtons.push_back(new DirButton(fileInfo[i].fullName, fileInfo[i].name, gridStyle, new UI::LinearLayoutParams(UI::FILL_PARENT, UI::FILL_PARENT))); } } else { - gameButtons.push_back(new GameButton(fileInfo[i].fullName, *gridStyle_, new UI::LinearLayoutParams(*gridStyle_ == true ? UI::WRAP_CONTENT : UI::FILL_PARENT, UI::WRAP_CONTENT))); + gameButtons.push_back(new GameButton(fileInfo[i].fullName, lastViewMode_, new UI::LinearLayoutParams(gridStyle ? UI::WRAP_CONTENT : UI::FILL_PARENT, UI::WRAP_CONTENT))); } } @@ -904,7 +997,7 @@ void GameBrowser::Refresh() { if (!fileInfo.empty()) { for (size_t i = 0; i < fileInfo.size(); i++) { if (!fileInfo[i].isDirectory) { - GameButton *b = new GameButton(fileInfo[i].fullName, *gridStyle_, new UI::LinearLayoutParams(UI::FILL_PARENT, UI::WRAP_CONTENT)); + GameButton *b = new GameButton(fileInfo[i].fullName, lastViewMode_, new UI::LinearLayoutParams(UI::FILL_PARENT, UI::WRAP_CONTENT)); b->SetHoldEnabled(false); gameButtons.push_back(b); } @@ -915,14 +1008,14 @@ void GameBrowser::Refresh() { if (browseFlags_ & BrowseFlags::NAVIGATE) { if (path_.CanNavigateUp()) { - gameList_->Add(new DirButton(Path(".."), *gridStyle_, new UI::LinearLayoutParams(UI::FILL_PARENT, UI::FILL_PARENT)))-> + gameList_->Add(new DirButton(Path(".."), gridStyle, new UI::LinearLayoutParams(UI::FILL_PARENT, UI::FILL_PARENT)))-> OnClick.Handle(this, &GameBrowser::NavigateClick); } // Add any pinned paths before other directories. auto pinnedPaths = GetPinnedPaths(); for (const auto &pinnedPath : pinnedPaths) { - DirButton *pinnedDir = gameList_->Add(new DirButton(pinnedPath, pinnedPath.GetFilename(), *gridStyle_, new UI::LinearLayoutParams(UI::FILL_PARENT, UI::FILL_PARENT))); + DirButton *pinnedDir = gameList_->Add(new DirButton(pinnedPath, pinnedPath.GetFilename(), gridStyle, new UI::LinearLayoutParams(UI::FILL_PARENT, UI::FILL_PARENT))); pinnedDir->OnClick.Handle(this, &GameBrowser::NavigateClick); pinnedDir->SetPinned(true); } @@ -950,7 +1043,7 @@ void GameBrowser::Refresh() { // Show a button to toggle pinning at the very end. if ((browseFlags_ & BrowseFlags::PIN) && !path_.GetPath().empty()) { std::string caption = ""; // IsCurrentPathPinned() ? "-" : "+"; - if (!*gridStyle_) { + if (!gridStyle) { caption = IsCurrentPathPinned() ? mm->T("UnpinPath", "Unpin") : mm->T("PinPath", "Pin"); } UI::Button *pinButton = gameList_->Add(new Button(caption, new UI::LinearLayoutParams(UI::FILL_PARENT, UI::FILL_PARENT))); diff --git a/UI/GameBrowser.h b/UI/GameBrowser.h index 05c843b066..357cdde1d0 100644 --- a/UI/GameBrowser.h +++ b/UI/GameBrowser.h @@ -6,6 +6,7 @@ #include "Common/File/Path.h" #include "Common/UI/ViewGroup.h" #include "Common/File/PathBrowser.h" +#include "Core/ConfigValues.h" #include "UI/MiscViews.h" enum GameBrowserFlags { @@ -26,7 +27,7 @@ ENUM_CLASS_BITOPS(BrowseFlags); class GameBrowser : public UI::LinearLayout { public: - GameBrowser(int token, const Path &path, BrowseFlags browseFlags, bool portrait, bool *gridStyle, ScreenManager *screenManager, std::string_view lastText, std::string_view lastLink, UI::LayoutParams *layoutParams = nullptr); + GameBrowser(int token, const Path &path, BrowseFlags browseFlags, bool portrait, int *viewMode, ScreenManager *screenManager, std::string_view lastText, std::string_view lastLink, UI::LayoutParams *layoutParams = nullptr); UI::Event OnChoice; UI::Event OnHoldChoice; @@ -53,6 +54,7 @@ protected: virtual Path HomePath(); void Refresh(); + GameBrowserViewMode ViewMode() const; Path homePath_; @@ -76,7 +78,7 @@ private: UI::ViewGroup *gameList_ = nullptr; PathBrowser path_; - bool *gridStyle_ = nullptr; + int *viewMode_ = nullptr; BrowseFlags browseFlags_; std::string lastText_; std::string lastLink_; @@ -87,7 +89,7 @@ private: bool listingPending_ = false; bool refreshPending_ = false; float lastScale_ = 1.0f; - bool lastLayoutWasGrid_ = true; + GameBrowserViewMode lastViewMode_ = GameBrowserViewMode::GRID; ScreenManager *screenManager_; int token_ = -1; bool portrait_ = false; diff --git a/UI/GameScreen.cpp b/UI/GameScreen.cpp index 85798c28e5..8779e22161 100644 --- a/UI/GameScreen.cpp +++ b/UI/GameScreen.cpp @@ -71,78 +71,27 @@ constexpr GameInfoFlags g_desiredFlags = GameInfoFlags::PARAM_SFO | GameInfoFlag class PMFView : public UI::InertView { public: - PMFView(std::string data, UI::LayoutParams *params) : UI::InertView(params), data_(data) { + PMFView(std::string data, UI::LayoutParams *params) : UI::InertView(params), video_(std::move(data)) { startTime_ = Instant::Now(); - player_ = pmf_create(); - if (0 == pmf_init(player_, (const uint8_t *)data_.data(), data_.size(), &width_, &height_)) { - - } else { - ERROR_LOG(Log::G3D, "Couldn't load pmf"); - pmf_destroy(player_); - player_ = nullptr; - } - } - - ~PMFView() { - if (curFrame_) { - curFrame_->Release(); - curFrame_ = nullptr; - } - - if (player_) { - pmf_destroy(player_); - player_ = nullptr; - } } void DeviceLost() override { - if (curFrame_) { - curFrame_->Release(); - curFrame_ = nullptr; - } + video_.DeviceLost(); } void GetContentDimensions(const UIContext &dc, float &w, float &h) const override { // Just use the video size. - w = width_; - h = height_; + w = video_.Width(); + h = video_.Height(); } void Draw(UIContext &dc) override { - if (!player_) { - return; - } - - Draw::DrawContext *draw = dc.GetDrawContext(); - - // Dimensions are capped by pmf_init, but use size_t arithmetic anyway - // so a regression can't overflow the allocation. - std::vector frame((size_t)width_ * (size_t)height_ * 4); - if (pmf_update(player_, startTime_.ElapsedSeconds(), frame.data())) { - if (curFrame_) { - curFrame_->Release(); - curFrame_ = nullptr; - } - - Draw::TextureDesc desc{}; - desc.width = width_; - desc.height = height_; - desc.depth = 1; - desc.format = Draw::DataFormat::R8G8B8A8_UNORM; - desc.generateMips = false; - desc.tag = "pmf_frame"; - desc.type = Draw::TextureType::LINEAR2D; - desc.mipLevels = 1; - desc.initData.push_back(frame.data()); - - curFrame_ = draw->CreateTexture(desc); - } - - if (curFrame_) { + Draw::Texture *frame = video_.Update(dc.GetDrawContext(), startTime_.ElapsedSeconds()); + if (frame) { int dropsize = 5; dc.Draw()->DrawImage4Grid(dc.GetTheme().dropShadow4Grid, bounds_.x - dropsize * 1.5f, bounds_.y - dropsize * 1.5f, bounds_.x2() + dropsize * 1.5f, bounds_.y2() + dropsize * 1.5f, 0xFF000000, 1.0f); dc.Flush(); - draw->BindTexture(0, curFrame_); + dc.GetDrawContext()->BindTexture(0, frame); dc.Draw()->DrawTexRect(bounds_, 0, 0, 1.0f, 1.0f, 0xFFFFFFFF); dc.Flush(); } @@ -151,12 +100,8 @@ public: } private: - std::string data_; - Draw::Texture *curFrame_ = nullptr; - PMFPlayer *player_ = nullptr; + PMFVideoTexture video_; Instant startTime_; - int width_ = 0; - int height_ = 0; }; GameScreen::GameScreen(const Path &gamePath, bool inGame) : UITwoPaneBaseDialogScreen(gamePath, TwoPaneFlags::SettingsToTheRight | TwoPaneFlags::CustomContextMenu), inGame_(inGame) { diff --git a/UI/MainScreen.cpp b/UI/MainScreen.cpp index c9524d1929..ff4b78b55d 100644 --- a/UI/MainScreen.cpp +++ b/UI/MainScreen.cpp @@ -177,7 +177,7 @@ void MainScreen::CreateRecentTab() { bool portrait = GetDeviceOrientation() == DeviceOrientation::Portrait; GameBrowser *tabRecentGames = new GameBrowser(GetRequesterToken(), - Path("!RECENT"), BrowseFlags::NONE, portrait, &g_Config.bGridView1, screenManager(), "", "", + Path("!RECENT"), BrowseFlags::NONE, portrait, &g_Config.iGameBrowserView1, screenManager(), "", "", new LinearLayoutParams(FILL_PARENT, WRAP_CONTENT)); tabRecentGames->SetSearchBar(search); @@ -190,7 +190,7 @@ void MainScreen::CreateRecentTab() { tabRecentGames->OnHighlight.Handle(this, &MainScreen::OnGameHighlight); } -GameBrowser *MainScreen::CreateBrowserTab(const Path &path, std::string_view title, std::string_view howToTitle, std::string_view howToUri, BrowseFlags browseFlags, bool *bGridView, float *scrollPos) { +GameBrowser *MainScreen::CreateBrowserTab(const Path &path, std::string_view title, std::string_view howToTitle, std::string_view howToUri, BrowseFlags browseFlags, int *viewMode, float *scrollPos) { using namespace UI; auto mm = GetI18NCategory(I18NCat::MAINMENU); @@ -203,7 +203,7 @@ GameBrowser *MainScreen::CreateBrowserTab(const Path &path, std::string_view tit ScrollView *scrollView = tabContainer->Add(new ScrollView(ORIENT_VERTICAL, new LinearLayoutParams(FILL_PARENT, WRAP_CONTENT, 1.0f))); scrollView->SetTag(title); // Re-use title as tag, should be fine. - GameBrowser *gameBrowser = new GameBrowser(GetRequesterToken(), path, browseFlags, portrait, bGridView, screenManager(), + GameBrowser *gameBrowser = new GameBrowser(GetRequesterToken(), path, browseFlags, portrait, viewMode, screenManager(), mm->T(howToTitle), howToUri, new LinearLayoutParams(FILL_PARENT, WRAP_CONTENT)); @@ -384,12 +384,12 @@ void MainScreen::CreateViews() { Button *focusButton = nullptr; if (hasStorageAccess) { - CreateBrowserTab(Path(g_Config.currentDirectory), "Games", "How to get games", getGamesUri, BrowseFlags::STANDARD, &g_Config.bGridView2, &g_Config.fGameListScrollPosition); - CreateBrowserTab(GetSysDirectory(DIRECTORY_GAME), "Homebrew & Demos", "How to get homebrew & demos", getHomebrewUri, BrowseFlags::HOMEBREW_STORE, &g_Config.bGridView3, &g_Config.fHomebrewScrollPosition); + CreateBrowserTab(Path(g_Config.currentDirectory), "Games", "How to get games", getGamesUri, BrowseFlags::STANDARD, &g_Config.iGameBrowserView2, &g_Config.fGameListScrollPosition); + CreateBrowserTab(GetSysDirectory(DIRECTORY_GAME), "Homebrew & Demos", "How to get homebrew & demos", getHomebrewUri, BrowseFlags::HOMEBREW_STORE, &g_Config.iGameBrowserView3, &g_Config.fHomebrewScrollPosition); if (g_Config.bRemoteTab && !g_Config.sLastRemoteISOServer.empty()) { Path remotePath(FormatRemoteISOUrl(g_Config.sLastRemoteISOServer.c_str(), g_Config.iLastRemoteISOPort, RemoteSubdir().c_str())); - GameBrowser *remoteBrowser = CreateBrowserTab(remotePath, "Remote disc streaming", "Remote disc streaming", remoteGamesUri, BrowseFlags::NAVIGATE, &g_Config.bGridView4, &g_Config.fRemoteScrollPosition); + GameBrowser *remoteBrowser = CreateBrowserTab(remotePath, "Remote disc streaming", "Remote disc streaming", remoteGamesUri, BrowseFlags::NAVIGATE, &g_Config.iGameBrowserView4, &g_Config.fRemoteScrollPosition); remoteBrowser->SetHomePath(remotePath); } @@ -860,7 +860,7 @@ void UmdReplaceScreen::CreateViews() { ScrollView *scrollRecentGames = new ScrollView(ORIENT_VERTICAL, new LinearLayoutParams(FILL_PARENT, WRAP_CONTENT)); scrollRecentGames->SetTag("UmdReplaceRecentGames"); GameBrowser *tabRecentGames = new GameBrowser(GetRequesterToken(), - Path("!RECENT"), BrowseFlags::NONE, portrait, &g_Config.bGridView1, screenManager(), "", "", + Path("!RECENT"), BrowseFlags::NONE, portrait, &g_Config.iGameBrowserView1, screenManager(), "", "", new LinearLayoutParams(FILL_PARENT, FILL_PARENT)); scrollRecentGames->Add(tabRecentGames); leftColumn->AddTab(mm->T("Recent"), ImageID::invalid(), scrollRecentGames); @@ -870,7 +870,7 @@ void UmdReplaceScreen::CreateViews() { ScrollView *scrollAllGames = new ScrollView(ORIENT_VERTICAL, new LinearLayoutParams(FILL_PARENT, WRAP_CONTENT)); scrollAllGames->SetTag("UmdReplaceAllGames"); - GameBrowser *tabAllGames = new GameBrowser(GetRequesterToken(), Path(g_Config.currentDirectory), BrowseFlags::STANDARD, portrait, &g_Config.bGridView2, screenManager(), + GameBrowser *tabAllGames = new GameBrowser(GetRequesterToken(), Path(g_Config.currentDirectory), BrowseFlags::STANDARD, portrait, &g_Config.iGameBrowserView2, screenManager(), mm->T("How to get games"), "https://www.ppsspp.org/getgames.html", new LinearLayoutParams(FILL_PARENT, FILL_PARENT)); @@ -932,9 +932,11 @@ void GridSettingsPopupScreen::CreatePopupContents(UI::ViewGroup *parent) { LinearLayout *items = new LinearLayoutList(ORIENT_VERTICAL); items->SetSpacing(0.0f); - items->Add(new CheckBox(&g_Config.bGridView1, sy->T("Display Recent on a grid"))); - items->Add(new CheckBox(&g_Config.bGridView2, sy->T("Display Games on a grid"))); - items->Add(new CheckBox(&g_Config.bGridView3, sy->T("Display Homebrew on a grid"))); + // Same order as GameBrowserViewMode. + static const char *viewModes[] = { "Grid", "List", "Large list" }; + items->Add(new PopupMultiChoice(&g_Config.iGameBrowserView1, mm->T("Recent"), viewModes, 0, ARRAY_SIZE(viewModes), I18NCat::SYSTEM, screenManager())); + items->Add(new PopupMultiChoice(&g_Config.iGameBrowserView2, mm->T("Games"), viewModes, 0, ARRAY_SIZE(viewModes), I18NCat::SYSTEM, screenManager())); + items->Add(new PopupMultiChoice(&g_Config.iGameBrowserView3, mm->T("Homebrew & Demos"), viewModes, 0, ARRAY_SIZE(viewModes), I18NCat::SYSTEM, screenManager())); static const char *defaultTabs[] = { "Recent", "Games", "Homebrew & Demos" }; PopupMultiChoice *beziersChoice = items->Add(new PopupMultiChoice(&g_Config.iDefaultTab, sy->T("Default tab"), defaultTabs, 0, ARRAY_SIZE(defaultTabs), I18NCat::MAINMENU, screenManager())); diff --git a/UI/MainScreen.h b/UI/MainScreen.h index 3181606103..2a9178e76a 100644 --- a/UI/MainScreen.h +++ b/UI/MainScreen.h @@ -58,7 +58,7 @@ protected: void CreateViews() override; void CreateRecentTab(); - GameBrowser *CreateBrowserTab(const Path &path, std::string_view title, std::string_view howToTitle, std::string_view howToUri, BrowseFlags browseFlags, bool *bGridView, float *scrollPos); + GameBrowser *CreateBrowserTab(const Path &path, std::string_view title, std::string_view howToTitle, std::string_view howToUri, BrowseFlags browseFlags, int *viewMode, float *scrollPos); void CreateMainButtons(UI::ViewGroup *parent, bool vertical); void DrawBackground(UIContext &dc) override; diff --git a/UI/MiscViews.cpp b/UI/MiscViews.cpp index a97b8fbc6c..57eacf7134 100644 --- a/UI/MiscViews.cpp +++ b/UI/MiscViews.cpp @@ -14,6 +14,7 @@ #include "Common/UI/PopupScreens.h" #include "Core/Config.h" #include "GPU/Common/PresentationCommon.h" +#include "Core/Util/VideoPlayer.h" TextWithImage::TextWithImage(ImageID imageID, std::string_view text, UI::LinearLayoutParams *layoutParams) : UI::LinearLayout(ORIENT_HORIZONTAL, layoutParams) { using namespace UI; @@ -509,3 +510,54 @@ bool ViewSearch::Key(UI::ViewGroup *viewGroup, const KeyInput &input) { } return retval; } + +PMFVideoTexture::PMFVideoTexture(std::string data) : data_(std::move(data)) { + player_ = pmf_create(); + if (pmf_init(player_, (const uint8_t *)data_.data(), data_.size(), &width_, &height_) != 0) { + ERROR_LOG(Log::G3D, "Couldn't load pmf"); + pmf_destroy(player_); + player_ = nullptr; + } +} + +PMFVideoTexture::~PMFVideoTexture() { + DeviceLost(); + if (player_) { + pmf_destroy(player_); + player_ = nullptr; + } +} + +void PMFVideoTexture::DeviceLost() { + if (curFrame_) { + curFrame_->Release(); + curFrame_ = nullptr; + } +} + +Draw::Texture *PMFVideoTexture::Update(Draw::DrawContext *draw, double timeSeconds) { + if (!player_) { + return nullptr; + } + + // Dimensions are capped by pmf_init, but use size_t arithmetic anyway + // so a regression can't overflow the allocation. + frame_.resize((size_t)width_ * (size_t)height_ * 4); + if (pmf_update(player_, timeSeconds, frame_.data())) { + DeviceLost(); + + Draw::TextureDesc desc{}; + desc.width = width_; + desc.height = height_; + desc.depth = 1; + desc.format = Draw::DataFormat::R8G8B8A8_UNORM; + desc.generateMips = false; + desc.tag = "pmf_frame"; + desc.type = Draw::TextureType::LINEAR2D; + desc.mipLevels = 1; + desc.initData.push_back(frame_.data()); + + curFrame_ = draw->CreateTexture(desc); + } + return curFrame_; +} diff --git a/UI/MiscViews.h b/UI/MiscViews.h index 709a7bf0da..50310bcf8c 100644 --- a/UI/MiscViews.h +++ b/UI/MiscViews.h @@ -132,3 +132,32 @@ struct ViewSearch { void ApplySearchFilter(UI::ViewGroup *viewGroup, bool setKeyboardFocus); bool Key(UI::ViewGroup *viewGroup, const KeyInput &input); }; + +struct PMFPlayer; +namespace Draw { +class DrawContext; +class Texture; +} + +// Decodes a PMF video (like a game's ICON1.PMF) into a texture, looping. +class PMFVideoTexture { +public: + explicit PMFVideoTexture(std::string data); + ~PMFVideoTexture(); + + bool IsValid() const { return player_ != nullptr; } + int Width() const { return width_; } + int Height() const { return height_; } + + // Returns the frame to show at the given time, or nullptr if none is decoded yet. + Draw::Texture *Update(Draw::DrawContext *draw, double timeSeconds); + void DeviceLost(); + +private: + std::string data_; + std::vector frame_; + Draw::Texture *curFrame_ = nullptr; + PMFPlayer *player_ = nullptr; + int width_ = 0; + int height_ = 0; +}; diff --git a/UI/RemoteISOScreen.cpp b/UI/RemoteISOScreen.cpp index 5e4a1739a1..3ba4d47d44 100644 --- a/UI/RemoteISOScreen.cpp +++ b/UI/RemoteISOScreen.cpp @@ -615,7 +615,7 @@ void RemoteISOBrowseScreen::CreateViews() { ScrollView *scrollRecentGames = new ScrollView(ORIENT_VERTICAL, new LinearLayoutParams(FILL_PARENT, WRAP_CONTENT)); scrollRecentGames->SetTag("RemoteGamesTab"); GameBrowser *tabRemoteGames = new GameBrowser(GetRequesterToken(), - Path(url_), BrowseFlags::NAVIGATE, portrait, &g_Config.bGridView1, screenManager(), "", "", + Path(url_), BrowseFlags::NAVIGATE, portrait, &g_Config.iGameBrowserView1, screenManager(), "", "", new LinearLayoutParams(FILL_PARENT, FILL_PARENT)); tabRemoteGames->SetHomePath(Path(url_)); diff --git a/UI/UIAtlas.cpp b/UI/UIAtlas.cpp index 5f68300638..3430114e95 100644 --- a/UI/UIAtlas.cpp +++ b/UI/UIAtlas.cpp @@ -76,6 +76,7 @@ static const ImageMeta g_uiImageIDs[] = { {"I_DROP_SHADOW", false}, {"I_LINES", false}, {"I_GRID", false}, + {"I_LARGE_LIST", false}, {"I_LOGO", false}, {"I_ICON", false}, {"I_ICON_GOLD", false}, diff --git a/assets/ui_images/images.svg b/assets/ui_images/images.svg index 13b8100a8d..cdb8c159e2 100644 --- a/assets/ui_images/images.svg +++ b/assets/ui_images/images.svg @@ -3,8 +3,8 @@ .st0{fill:#000000;} - +