From 383f99111f3b054c5c51622a02b03dec68eb02dc Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Henrik=20Rydg=C3=A5rd?= Date: Sat, 31 Oct 2026 13:36:16 +0100 Subject: [PATCH 1/8] Break out the icon picker, some renaming --- UI/GameBrowser.cpp | 99 ++++++++++++++++++++++++---------------------- 1 file changed, 52 insertions(+), 47 deletions(-) diff --git a/UI/GameBrowser.cpp b/UI/GameBrowser.cpp index edcff49bba..ffb101f241 100644 --- a/UI/GameBrowser.cpp +++ b/UI/GameBrowser.cpp @@ -382,6 +382,44 @@ void GameButton::UpdateVideo(const std::shared_ptr &ginfo, bool wantVi } } +// These are the built-in icons used for file types that don't have one (or simply files missing one). +static ImageID ChooseGameIcon(const std::shared_ptr &ginfo) { + if (ginfo->GetFilePath().Type() == PathType::HTTP) { + return ginfo->fileType == IdentifiedFileType::UNKNOWN ? ImageID("I_LINK_OUT_QUESTION") : ImageID("I_LINK_OUT"); + } + + switch (ginfo->fileType) { + case IdentifiedFileType::PSP_ELF: + if (ginfo->region == GameRegion::VSH) { + return ImageID::invalid(); // it has a (preset) icon. + } + // 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::PPSSPP_GE_DUMP: return ImageID("I_DISPLAY"); break; + case IdentifiedFileType::PSX_ISO: + case IdentifiedFileType::PSP_PS1_PBP: return ImageID("I_PSX_ISO"); break; + case IdentifiedFileType::PS2_ISO: return ImageID("I_PS2_ISO"); break; + case IdentifiedFileType::PS3_ISO: return ImageID("I_PS3_ISO"); break; + case IdentifiedFileType::PSP_UMD_VIDEO_ISO: return ImageID("I_UMD_VIDEO_ISO"); break; + case IdentifiedFileType::UNKNOWN_ISO: return ImageID("I_UNKNOWN_ISO"); break; + case IdentifiedFileType::PPSSPP_SAVESTATE: + case IdentifiedFileType::ERROR_IDENTIFYING: + case IdentifiedFileType::UNKNOWN_BIN: return ImageID("I_FILE"); break; + case IdentifiedFileType::ARCHIVE_ZIP: return ImageID("I_ARCHIVE_ZIP"); break; + case IdentifiedFileType::ARCHIVE_7Z: return ImageID("I_ARCHIVE_7Z"); break; + case IdentifiedFileType::ARCHIVE_RAR: return ImageID("I_ARCHIVE_RAR"); break; + case IdentifiedFileType::PSP_PKG: return ImageID("I_FOLDER_UPLOAD"); break; + + case IdentifiedFileType::PSP_ISO: + case IdentifiedFileType::PSP_ISO_NP: + return ImageID("I_APP"); // This will practically always be replaced by a real icon. + + default: + return ImageID::invalid(); + } +} + void GameButton::Draw(UIContext &dc) { // Like the XMB, only the selected game plays its video. Only keyboard/gamepad focus selects, since with // touch or the mouse, getting a video to play without launching the game was too fiddly. @@ -403,54 +441,21 @@ void GameButton::Draw(UIContext &dc) { } // Some types we just draw a default icon for. - ImageID imageIcon = ImageID::invalid(); - bool drawBackground = true; - switch (ginfo->fileType) { - case IdentifiedFileType::PSP_ELF: - imageIcon = ImageID("I_APP"); - // The XMB is an ELF too, but has an icon. - drawBackground = ginfo->region == GameRegion::VSH; - break; - case IdentifiedFileType::UNKNOWN_ELF: imageIcon = ImageID("I_APP"); drawBackground = false; break; - case IdentifiedFileType::PPSSPP_GE_DUMP: imageIcon = ImageID("I_DISPLAY"); break; - case IdentifiedFileType::PSX_ISO: - case IdentifiedFileType::PSP_PS1_PBP: imageIcon = ImageID("I_PSX_ISO"); break; - case IdentifiedFileType::PS2_ISO: imageIcon = ImageID("I_PS2_ISO"); break; - case IdentifiedFileType::PS3_ISO: imageIcon = ImageID("I_PS3_ISO"); break; - case IdentifiedFileType::PSP_UMD_VIDEO_ISO: imageIcon = ImageID("I_UMD_VIDEO_ISO"); break; - case IdentifiedFileType::UNKNOWN_ISO: imageIcon = ImageID("I_UNKNOWN_ISO"); break; - case IdentifiedFileType::PPSSPP_SAVESTATE: - case IdentifiedFileType::ERROR_IDENTIFYING: - case IdentifiedFileType::UNKNOWN_BIN: imageIcon = ImageID("I_FILE"); break; - case IdentifiedFileType::ARCHIVE_ZIP: imageIcon = ImageID("I_ARCHIVE_ZIP"); drawBackground = false; break; - case IdentifiedFileType::ARCHIVE_7Z: imageIcon = ImageID("I_ARCHIVE_7Z"); drawBackground = false; break; - case IdentifiedFileType::ARCHIVE_RAR: imageIcon = ImageID("I_ARCHIVE_RAR"); drawBackground = false; break; - case IdentifiedFileType::PSP_PKG: imageIcon = ImageID("I_FOLDER_UPLOAD"); drawBackground = false; break; - default: break; - } + ImageID imageIcon = ChooseGameIcon(ginfo); if (ginfo->Ready(GameInfoFlags::ICON)) { - if (ginfo->icon.texture && drawBackground) { + if (ginfo->icon.texture) { 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 if (drawBackground) { + } else { // No icon, but drawBackground is set. Let's show a plain icon depending on type. - if (ginfo->fileType == IdentifiedFileType::PSP_ISO || ginfo->fileType == IdentifiedFileType::PSP_ISO_NP) { - imageIcon = ImageID("I_UMD"); - } else if (ginfo->fileType == IdentifiedFileType::PSX_ISO || ginfo->fileType == IdentifiedFileType::PSP_PS1_PBP) { - imageIcon = ImageID("I_PSX_ISO"); - } else if (gamePath_.Type() == PathType::HTTP) { - imageIcon = ginfo->fileType == IdentifiedFileType::UNKNOWN ? ImageID("I_LINK_OUT_QUESTION") : ImageID("I_LINK_OUT"); - } else { - imageIcon = ImageID("I_APP"); - } + imageIcon = ChooseGameIcon(ginfo); } - } else { + } else if (gamePath_.Type() == PathType::HTTP) { // Make sure that pending HTTP icons show something, instead of just blank. - if (gamePath_.Type() == PathType::HTTP) { - imageIcon = ImageID("I_LINK_OUT_QUESTION"); - } + // Maybe we should show something for other file types too. + imageIcon = ImageID("I_LINK_OUT_QUESTION"); } Bounds overlayBounds = bounds_; @@ -651,16 +656,16 @@ void GameButton::Draw(UIContext &dc) { ImageID("I_FLAG_AS"), ImageID("I_FLAG_KO"), }; - const AtlasImage *image = dc.Draw()->GetAtlas()->getImage(regionIcons[regionIndex]); - if (image) { + const AtlasImage *regionImage = dc.Draw()->GetAtlas()->getImage(regionIcons[regionIndex]); + if (regionImage) { if (viewMode_ == GameBrowserViewMode::GRID) { - 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); + dc.Draw()->DrawImage(regionIcons[regionIndex], bounds_.x + bounds_.w - (regionImage->w + 5) * g_Config.fGameGridScale, + y + h - (regionImage->h + 5) * g_Config.fGameGridScale, g_Config.fGameGridScale); } else if (viewMode_ == GameBrowserViewMode::LARGE_LIST) { - dc.Draw()->DrawImage(regionIcons[regionIndex], bounds_.x + LARGE_LIST_LEFT_PAD + (LARGE_LIST_ICON_W - image->w - 5) * g_Config.fGameGridScale, - y + h - (image->h + 5) * g_Config.fGameGridScale, g_Config.fGameGridScale); + dc.Draw()->DrawImage(regionIcons[regionIndex], bounds_.x + LARGE_LIST_LEFT_PAD + (LARGE_LIST_ICON_W - regionImage->w - 5) * g_Config.fGameGridScale, + y + h - (regionImage->h + 5) * g_Config.fGameGridScale, g_Config.fGameGridScale); } else { - dc.Draw()->DrawImage(regionIcons[regionIndex], bounds_.x + 4, y + h - image->h - 5, 1.0f); + dc.Draw()->DrawImage(regionIcons[regionIndex], bounds_.x + 4, y + h - regionImage->h - 5, 1.0f); } } } From 22e67da85c63d0ea9e21985063492852a8c53d44 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Henrik=20Rydg=C3=A5rd?= Date: Sat, 31 Oct 2026 19:53:51 +0100 Subject: [PATCH 2/8] More code cleanup --- UI/GameBrowser.cpp | 57 +++++++++++++++++++++++----------------------- UI/MiscViews.cpp | 2 +- 2 files changed, 29 insertions(+), 30 deletions(-) diff --git a/UI/GameBrowser.cpp b/UI/GameBrowser.cpp index ffb101f241..4636427c35 100644 --- a/UI/GameBrowser.cpp +++ b/UI/GameBrowser.cpp @@ -80,12 +80,9 @@ static float ListTextRightMargin(GameBrowserViewMode viewMode) { // In the list views, rightText (like a game ID) goes at the right edge, as for a game with a picture. static void DrawIconWithText(UIContext &dc, ImageID image, std::string_view text, const Bounds &bounds, GameBrowserViewMode viewMode, const UI::Style &style, std::string_view rightText = {}) { - 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); - - const bool compact = bounds.w < 180 * (gridStyle ? g_Config.fGameGridScale : 1.0); - if (compact) { + if (viewMode == GameBrowserViewMode::GRID) { + float tw, th; + dc.MeasureText(dc.GetFontStyle(), g_Config.fGameGridScale, g_Config.fGameGridScale, text, &tw, &th, 0); dc.PushScissor(bounds); const FontStyle *fontStyle = GetTextStyle(dc, UI::TextSize::Small); dc.SetFontStyle(*GetTextStyle(dc, UI::TextSize::Small)); @@ -113,12 +110,16 @@ static void DrawIconWithText(UIContext &dc, ImageID image, std::string_view text dc.SetFontStyle(dc.GetTheme().uiFont); dc.PopScissor(); } else { + float tw, th; + // List, large list + dc.MeasureText(dc.GetFontStyle(), 1.0f, 1.0f, text, &tw, &th, 0); + const float textX = ListTextX(viewMode); if (viewMode == GameBrowserViewMode::LARGE_LIST) { const float scale = g_Config.fGameGridScale; dc.Draw()->DrawImage(image, bounds.x + LARGE_LIST_LEFT_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); + dc.Draw()->DrawImage(image, bounds.x + 72, bounds.centerY(), 0.88f, style.fgColor, ALIGN_CENTER); } float tx = textX; @@ -442,7 +443,6 @@ void GameButton::Draw(UIContext &dc) { // Some types we just draw a default icon for. ImageID imageIcon = ChooseGameIcon(ginfo); - if (ginfo->Ready(GameInfoFlags::ICON)) { if (ginfo->icon.texture) { texture = ginfo->icon.texture; @@ -458,22 +458,6 @@ void GameButton::Draw(UIContext &dc) { imageIcon = ImageID("I_LINK_OUT_QUESTION"); } - Bounds overlayBounds = bounds_; - u32 overlayColor = 0; - if (holdEnabled_ && holdStart_ != 0.0) { - double time_held = time_now_d() - holdStart_; - overlayColor = whiteAlpha(time_held / 2.5f); - if (holdStart_ != 0.0) { - double time_held = time_now_d() - holdStart_; - int holdFrameCount = (int)(time_held * 60.0f); - if (holdFrameCount > 60) { - // Blink before launching by holding - if (((holdFrameCount >> 3) & 1) == 0) - overlayColor = 0x0; - } - } - } - int x = bounds_.x; int y = bounds_.y; int w = viewMode_ == GameBrowserViewMode::GRID ? bounds_.w : 144; @@ -520,7 +504,11 @@ void GameButton::Draw(UIContext &dc) { } int txOffset = down_ ? 4 : 0; - if (viewMode_ != GameBrowserViewMode::GRID) txOffset = 0; + if (viewMode_ != GameBrowserViewMode::GRID) { + txOffset = 0; + } + + Bounds overlayBounds = bounds_; // Render button int dropsize = 10; @@ -564,6 +552,21 @@ void GameButton::Draw(UIContext &dc) { dc.SetFontScale(1.0f, 1.0f); } + u32 overlayColor = 0; + if (holdEnabled_ && holdStart_ != 0.0) { + double time_held = time_now_d() - holdStart_; + overlayColor = whiteAlpha(time_held / 2.5f); + if (holdStart_ != 0.0) { + double time_held = time_now_d() - holdStart_; + int holdFrameCount = (int)(time_held * 60.0f); + if (holdFrameCount > 60) { + // Blink before launching by holding + if (((holdFrameCount >> 3) & 1) == 0) + overlayColor = 0x0; + } + } + } + if (imageIcon.isValid()) { Style style = dc.GetTheme().itemStyle; @@ -572,10 +575,6 @@ void GameButton::Draw(UIContext &dc) { if (!IsEnabled()) style = dc.GetTheme().itemDisabledStyle; DrawIconWithText(dc, imageIcon, ginfo->GetTitle(), bounds_, viewMode_, style, g_Config.bShowIDOnGameIcon ? std::string_view(ginfo->id_version) : std::string_view()); - - if (overlayColor) { - dc.FillRect(Drawable(overlayColor), overlayBounds); - } } else { char discNumInfo[8]; if (ginfo->disc_total > 1) diff --git a/UI/MiscViews.cpp b/UI/MiscViews.cpp index ec5fa7ec97..a95bc6fa30 100644 --- a/UI/MiscViews.cpp +++ b/UI/MiscViews.cpp @@ -67,7 +67,7 @@ TopBar::TopBar(const UIContext &ctx, TopBarFlags flags, std::string_view title, } if (!title.empty()) { - TextView *titleView = Add(new TextView(title, ALIGN_VCENTER, false, new LinearLayoutParams(1.0f, Gravity::G_VCENTER))); + TextView *titleView = Add(new TextView(title, ALIGN_VCENTER | FLAG_ELLIPSIZE_TEXT, false, new LinearLayoutParams(1.0f, Gravity::G_VCENTER))); titleView->SetTextColor(ctx.GetTheme().itemStyle.fgColor); titleView->SetBig(true); if (flags & TopBarFlags::DropShadowTitle) { From 77904479f951b2eb84771c56828fae96c457fadf Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Henrik=20Rydg=C3=A5rd?= Date: Sat, 31 Oct 2026 23:37:48 +0100 Subject: [PATCH 3/8] Refactor work --- UI/EmuScreen.cpp | 4 +- UI/GameBrowser.cpp | 151 ++++++++++++++++++++++++--------------------- UI/PauseScreen.cpp | 2 +- 3 files changed, 83 insertions(+), 74 deletions(-) diff --git a/UI/EmuScreen.cpp b/UI/EmuScreen.cpp index f033b153a2..4c1ab9c18b 100644 --- a/UI/EmuScreen.cpp +++ b/UI/EmuScreen.cpp @@ -665,7 +665,7 @@ void EmuScreen::sendMessage(UIMessage message, const char *value) { // temporary workaround for hotkey its freeze the ui when open chat screen using hotkey and native keyboard is enable if (g_Config.bBypassOSKWithKeyboard) { // TODO: Make translatable. - g_OSD.Show(OSDType::MESSAGE_INFO, "Disable \"Use system native keyboard\" to use chat hotkey", 2.0f); + g_OSD.Show(OSDType::MESSAGE_INFO, "Disable \"Use system native keyboard\" to use chat hotkey", 2.0f, "chatHotKey"); } else { OpenChat(true); } @@ -943,7 +943,7 @@ void EmuScreen::ProcessVKey(VirtKey virtKey, bool down) { if (SaveState::CanRewind()) { SaveState::Rewind(&ShowMessageAfterSaveStateAction); } else { - g_OSD.Show(OSDType::MESSAGE_WARNING, sc->T("norewind", "No rewind save states available"), 2.0); + g_OSD.Show(OSDType::MESSAGE_WARNING, sc->T("norewind", "No rewind save states available"), 2.0, "norewind"); } } } diff --git a/UI/GameBrowser.cpp b/UI/GameBrowser.cpp index 4636427c35..d6b3c6cd07 100644 --- a/UI/GameBrowser.cpp +++ b/UI/GameBrowser.cpp @@ -78,75 +78,6 @@ static float ListTextRightMargin(GameBrowserViewMode viewMode) { return 0.0f; } -// In the list views, rightText (like a game ID) goes at the right edge, as for a game with a picture. -static void DrawIconWithText(UIContext &dc, ImageID image, std::string_view text, const Bounds &bounds, GameBrowserViewMode viewMode, const UI::Style &style, std::string_view rightText = {}) { - if (viewMode == GameBrowserViewMode::GRID) { - float tw, th; - dc.MeasureText(dc.GetFontStyle(), g_Config.fGameGridScale, g_Config.fGameGridScale, text, &tw, &th, 0); - dc.PushScissor(bounds); - const FontStyle *fontStyle = GetTextStyle(dc, UI::TextSize::Small); - dc.SetFontStyle(*GetTextStyle(dc, UI::TextSize::Small)); - - int iconSize = image == ImageID("I_UP_DIRECTORY") ? (float)dc.Draw()->GetAtlas()->getImage(image)->h : bounds.h * 0.3f; - float textWidth = 0.0f; - float textHeight = 0; - dc.MeasureTextRect(*fontStyle, 1.0f, 1.0f, text, bounds.w - 10, &textWidth, &textHeight, ALIGN_HCENTER | FLAG_WRAP_TEXT); - - int totalHeight = iconSize + (int)textHeight; - - const float y = std::max(0.0f, bounds.h / 2.0f - totalHeight / 2.0f); - - if (image.isValid()) { - const AtlasImage *img = dc.Draw()->GetAtlas()->getImage(image); - if (img && img->h > 0) { - dc.RebindTexture(); - dc.Draw()->DrawImage(image, bounds.centerX(), bounds.y + y + 2, iconSize / (float)img->h, style.fgColor, ALIGN_TOP | ALIGN_HCENTER); - } - } - - if (image != ImageID("I_UP_DIRECTORY") && image != ImageID("I_PIN") && image != ImageID("I_UNPIN")) { - dc.DrawTextRect(text, bounds.Inset(5, y + iconSize + 4, 5, 2), style.fgColor, ALIGN_HCENTER | FLAG_WRAP_TEXT); - } - dc.SetFontStyle(dc.GetTheme().uiFont); - dc.PopScissor(); - } else { - float tw, th; - // List, large list - dc.MeasureText(dc.GetFontStyle(), 1.0f, 1.0f, text, &tw, &th, 0); - - const float textX = ListTextX(viewMode); - if (viewMode == GameBrowserViewMode::LARGE_LIST) { - const float scale = g_Config.fGameGridScale; - dc.Draw()->DrawImage(image, bounds.x + LARGE_LIST_LEFT_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, style.fgColor, ALIGN_CENTER); - } - - float tx = textX; - int availableWidth = bounds.w - textX - ListTextRightMargin(viewMode); - if (!rightText.empty()) { - float vw, vh; - dc.MeasureText(dc.GetFontStyle(), 0.7f, 0.7f, rightText, &vw, &vh, 0); - availableWidth -= vw + 20; - dc.SetFontScale(0.7f, 0.7f); - dc.DrawText(rightText, 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; - if (availableWidth < tw) { - tx -= (1.0f + sin(time_now_d() * 1.5f)) * sineWidth; - Bounds tb = bounds; - tb.x = bounds.x + textX; - tb.w = availableWidth; - dc.PushScissor(tb); - } - dc.DrawText(text, bounds.x + tx, bounds.centerY(), style.fgColor, ALIGN_VCENTER | FLAG_WRAP_TEXT); - if (availableWidth < tw) { - dc.PopScissor(); - } - } -} - class GameButton : public UI::Clickable { public: GameButton(const Path &gamePath, GameBrowserViewMode viewMode, UI::LayoutParams *layoutParams = nullptr) @@ -421,6 +352,76 @@ static ImageID ChooseGameIcon(const std::shared_ptr &ginfo) { } } +// Grid style. Fallback +static void DrawIconWithTextGrid(UIContext &dc, ImageID image, std::string_view text, const Bounds &bounds, const UI::Style &style) { + float tw, th; + dc.MeasureText(dc.GetFontStyle(), g_Config.fGameGridScale, g_Config.fGameGridScale, text, &tw, &th, 0); + dc.PushScissor(bounds); + const FontStyle *fontStyle = GetTextStyle(dc, UI::TextSize::Small); + dc.SetFontStyle(*GetTextStyle(dc, UI::TextSize::Small)); + + int iconSize = image == ImageID("I_UP_DIRECTORY") ? (float)dc.Draw()->GetAtlas()->getImage(image)->h : bounds.h * 0.3f; + float textWidth = 0.0f; + float textHeight = 0; + dc.MeasureTextRect(*fontStyle, 1.0f, 1.0f, text, bounds.w - 10, &textWidth, &textHeight, ALIGN_HCENTER | FLAG_WRAP_TEXT); + + int totalHeight = iconSize + (int)textHeight; + + const float y = std::max(0.0f, bounds.h / 2.0f - totalHeight / 2.0f); + + if (image.isValid()) { + const AtlasImage *img = dc.Draw()->GetAtlas()->getImage(image); + if (img && img->h > 0) { + dc.RebindTexture(); + dc.Draw()->DrawImage(image, bounds.centerX(), bounds.y + y + 2, iconSize / (float)img->h, style.fgColor, ALIGN_TOP | ALIGN_HCENTER); + } + } + + if (image != ImageID("I_UP_DIRECTORY") && image != ImageID("I_PIN") && image != ImageID("I_UNPIN")) { + dc.DrawTextRect(text, bounds.Inset(5, y + iconSize + 4, 5, 2), style.fgColor, ALIGN_HCENTER | FLAG_WRAP_TEXT); + } + dc.SetFontStyle(dc.GetTheme().uiFont); + dc.PopScissor(); +} + +// List, large list +// In the list views, rightText (like a game ID) goes at the right edge, as for a game with a picture. +static void DrawIconWithTextList(UIContext &dc, ImageID image, std::string_view text, const Bounds &bounds, GameBrowserViewMode viewMode, const UI::Style &style, std::string_view rightText = {}) { + float tw, th; + dc.MeasureText(dc.GetFontStyle(), 1.0f, 1.0f, text, &tw, &th, 0); + + const float textX = ListTextX(viewMode); + if (viewMode == GameBrowserViewMode::LARGE_LIST) { + const float scale = g_Config.fGameGridScale; + dc.Draw()->DrawImage(image, bounds.x + LARGE_LIST_LEFT_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, style.fgColor, ALIGN_CENTER); + } + + float tx = textX; + int availableWidth = bounds.w - textX - ListTextRightMargin(viewMode); + if (!rightText.empty()) { + float vw, vh; + dc.MeasureText(dc.GetFontStyle(), 0.7f, 0.7f, rightText, &vw, &vh, 0); + availableWidth -= vw + 20; + dc.SetFontScale(0.7f, 0.7f); + dc.DrawText(rightText, 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; + if (availableWidth < tw) { + tx -= (1.0f + sin(time_now_d() * 1.5f)) * sineWidth; + Bounds tb = bounds; + tb.x = bounds.x + textX; + tb.w = availableWidth; + dc.PushScissor(tb); + } + dc.DrawText(text, bounds.x + tx, bounds.centerY(), style.fgColor, ALIGN_VCENTER | FLAG_WRAP_TEXT); + if (availableWidth < tw) { + dc.PopScissor(); + } +} + void GameButton::Draw(UIContext &dc) { // Like the XMB, only the selected game plays its video. Only keyboard/gamepad focus selects, since with // touch or the mouse, getting a video to play without launching the game was too fiddly. @@ -574,7 +575,11 @@ void GameButton::Draw(UIContext &dc) { if (down_) style = dc.GetTheme().itemDownStyle; if (!IsEnabled()) style = dc.GetTheme().itemDisabledStyle; - DrawIconWithText(dc, imageIcon, ginfo->GetTitle(), bounds_, viewMode_, style, g_Config.bShowIDOnGameIcon ? std::string_view(ginfo->id_version) : std::string_view()); + if (viewMode_ == GameBrowserViewMode::GRID) { + DrawIconWithTextGrid(dc, imageIcon, ginfo->GetTitle(), bounds_, style); + } else { + DrawIconWithTextList(dc, imageIcon, ginfo->GetTitle(), bounds_, viewMode_, style, g_Config.bShowIDOnGameIcon ? std::string_view(ginfo->id_version) : std::string_view()); + } } else { char discNumInfo[8]; if (ginfo->disc_total > 1) @@ -768,7 +773,11 @@ void DirButton::Draw(UIContext &dc) { if (!IsEnabled()) style = dc.GetTheme().itemDisabledStyle; dc.FillRect(style.background, bounds_); - DrawIconWithText(dc, image, text, bounds_, viewMode_, style); + if (viewMode_ == GameBrowserViewMode::GRID) { + DrawIconWithTextGrid(dc, image, text, bounds_, style); + } else { + DrawIconWithTextList(dc, image, text, bounds_, viewMode_, style); + } } 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) diff --git a/UI/PauseScreen.cpp b/UI/PauseScreen.cpp index f83c6cbc67..5208b93a8d 100644 --- a/UI/PauseScreen.cpp +++ b/UI/PauseScreen.cpp @@ -83,7 +83,7 @@ void copyDeepLinkForPath(std::string_view filePath); void ShowMessageAfterSaveStateAction(SaveState::Status status, std::string_view message, std::string_view metadata) { if (!message.empty()) { g_OSD.Show(status == SaveState::Status::SUCCESS ? OSDType::MESSAGE_SUCCESS : OSDType::MESSAGE_ERROR, - message, metadata, status == SaveState::Status::SUCCESS ? 2.0 : 5.0); + message, metadata, status == SaveState::Status::SUCCESS ? 2.0 : 5.0, "savestateaction"); } } From 42d3206bcf3c3e91adbe8030ed6d3a16bec6d454 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Henrik=20Rydg=C3=A5rd?= Date: Sun, 1 Nov 2026 00:06:19 +0100 Subject: [PATCH 4/8] Move GameButton out of GameBrowser.cpp into its own file The icon-with-text drawing helpers and the large list dimensions move with it, exported since DirButton uses them too. Co-Authored-By: Claude Opus 5.5 --- UI/CMakeLists.txt | 2 + UI/GameBrowser.cpp | 664 +----------------------------- UI/GameButton.cpp | 471 +++++++++++++++++++++ UI/GameButton.h | 262 ++++++++++++ UI/UI.vcxproj | 2 + UI/UI.vcxproj.filters | 6 + UWP/UI_UWP/UI_UWP.vcxproj | 2 + UWP/UI_UWP/UI_UWP.vcxproj.filters | 2 + android/jni/Android.mk | 1 + 9 files changed, 749 insertions(+), 663 deletions(-) create mode 100644 UI/GameButton.cpp create mode 100644 UI/GameButton.h diff --git a/UI/CMakeLists.txt b/UI/CMakeLists.txt index 7ed465f3e1..2a6f150406 100644 --- a/UI/CMakeLists.txt +++ b/UI/CMakeLists.txt @@ -53,6 +53,8 @@ list(APPEND UISource MainScreen.cpp GameBrowser.h GameBrowser.cpp + GameButton.h + GameButton.cpp MiscScreens.h MiscScreens.cpp MiscViews.h diff --git a/UI/GameBrowser.cpp b/UI/GameBrowser.cpp index d6b3c6cd07..5ee36c6846 100644 --- a/UI/GameBrowser.cpp +++ b/UI/GameBrowser.cpp @@ -27,7 +27,6 @@ #include "Common/UI/Root.h" #include "Common/UI/ScreenManager.h" -#include "Common/Math/curves.h" #include "Common/Net/URL.h" #include "Common/File/FileUtil.h" #include "Common/TimeUtil.h" @@ -40,10 +39,9 @@ #include "Core/ELF/PBPReader.h" #include "Core/FileLoaders/PS1EbootFileLoader.h" #include "Core/MemMap.h" -#include "Core/Util/VideoPlayer.h" #include "UI/GameBrowser.h" -#include "UI/GamePacker.h" +#include "UI/GameButton.h" #include "UI/RemoteISOScreen.h" #include "UI/SavedataScreen.h" #include "UI/Store.h" @@ -53,669 +51,9 @@ #include "Common/Data/Text/I18n.h" #include "Core/Util/DarwinFileSystemServices.h" // For the browser -// 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; -static constexpr float LARGE_LIST_LEFT_PAD = 11.0f; -static constexpr float LARGE_LIST_INFO_SIZE = 64.0f; // Folders get bigger lines too, though not as big as the games. static constexpr float LARGE_LIST_FOLDER_H = 50.0f; -// Where the text starts in the list modes. -static float ListTextX(GameBrowserViewMode viewMode) { - if (viewMode == GameBrowserViewMode::LARGE_LIST) { - return LARGE_LIST_LEFT_PAD + LARGE_LIST_ICON_W * g_Config.fGameGridScale + 20.0f; - } - return 150.0f; -} - -// Space to the right of the text in the list modes, for the info button. -static float ListTextRightMargin(GameBrowserViewMode viewMode) { - if (viewMode == GameBrowserViewMode::LARGE_LIST) { - return LARGE_LIST_INFO_SIZE + LARGE_LIST_PAD * 2; - } - return 0.0f; -} - -class GameButton : public UI::Clickable { -public: - GameButton(const Path &gamePath, GameBrowserViewMode viewMode, UI::LayoutParams *layoutParams = nullptr) - : UI::Clickable(layoutParams), viewMode_(viewMode), gamePath_(gamePath) {} - - void Draw(UIContext &dc) override; - std::string DescribeText() const override; - std::string SearchAlias() const override { - // Without the extension, or searching for "iso" would match every ISO. Only three or more - // letters counts as one, so a folder like "Game v1.2" keeps its version number. - std::string filename = gamePath_.GetFilename(); - const std::string ext = gamePath_.GetFileExtension(); - if (ext.size() >= 4 && ext.size() <= filename.size() && std::all_of(ext.begin() + 1, ext.end(), [](char c) { return c >= 'a' && c <= 'z'; })) { - filename.resize(filename.size() - ext.size()); - } - return filename; - } - void GetContentDimensions(const UIContext &dc, float &w, float &h) const override { - switch (viewMode_) { - case GameBrowserViewMode::GRID: - w = 144 * g_Config.fGameGridScale; - h = 80 * g_Config.fGameGridScale; - break; - case GameBrowserViewMode::LARGE_LIST: - w = 500; - h = LARGE_LIST_ICON_H * g_Config.fGameGridScale + LARGE_LIST_PAD * 2; - break; - default: // List - w = 500; - h = 50; - break; - } - } - const Path &GamePath() const { return gamePath_; } - - void SetHoldEnabled(bool hold) { - holdEnabled_ = hold; - } - bool Touch(const TouchInput &input) override { - hovering_ = bounds_.Contains(input.x, input.y); - if (HasInfoButton() && TouchInfoButton(input)) { - return true; - } - const bool retval = UI::Clickable::Touch(input); - if (hovering_ && (input.flags & TouchInputFlags::DOWN)) { - holdStart_ = time_now_d(); - } - if (input.flags & TouchInputFlags::UP) { - holdStart_ = 0; - } - return retval; - } - - bool Key(const KeyInput &key) override { - bool showInfo = false; - - if (HasFocus() && UI::IsInfoKey(key)) { - // If it's the button that's mapped to triangle, then show the info. - if (key.flags & KeyInputFlags::DOWN) { - infoKeyPressed_ = true; - } - if ((key.flags & KeyInputFlags::UP) && infoKeyPressed_) { - showInfo = true; - infoKeyPressed_ = false; - } - } else if (hovering_ && key.deviceId == DEVICE_ID_MOUSE && key.keyCode == NKCODE_EXT_MOUSEBUTTON_2) { - // If it's the right mouse button, and it's not otherwise mapped, show the info also. - if (key.flags & KeyInputFlags::DOWN) { - showInfoPressed_ = true; - } - if ((key.flags & KeyInputFlags::UP) && showInfoPressed_) { - showInfo = true; - showInfoPressed_ = false; - } - } - - if (showInfo) { - TriggerOnHoldClick(); - return true; - } - - if (infoSelected_ && HasFocus() && UI::IsAcceptKey(key)) { - if (key.flags & KeyInputFlags::DOWN) { - infoButtonDown_ = true; - } - if ((key.flags & KeyInputFlags::UP) && infoButtonDown_) { - infoButtonDown_ = false; - TriggerOnHoldClick(); - } - return true; - } - - // Holding the accept button works like a long press, so a gamepad can do it too. - if (holdEnabled_ && HasFocus() && UI::IsAcceptKey(key) && !(key.flags & KeyInputFlags::IS_REPEAT)) { - if (key.flags & KeyInputFlags::DOWN) { - holdStart_ = time_now_d(); - } - if (key.flags & KeyInputFlags::UP) { - holdStart_ = 0.0; - } - } - - return Clickable::Key(key); - } - - void Update() override { - // Hold button for 1.5 seconds to launch the game options - if (holdEnabled_ && holdStart_ != 0.0 && holdStart_ < time_now_d() - 1.5) { - TriggerOnHoldClick(); - } - } - - void FocusChanged(UI::FocusFlags focusFlags) override { - UI::Clickable::FocusChanged(focusFlags); - if (focusFlags & UI::FocusFlags::LOST_FOCUS) { - // Don't let a held accept button carry over from a button we've left. - holdStart_ = 0.0; - // Moving to another game selects the game, not its info button. - infoSelected_ = false; - infoButtonDown_ = false; - } - TriggerOnHighlight(focusFlags); - } - - // Left and right move between the game and its info button, so up and down still go from game to game. - bool CanMoveFocus(UI::FocusMove dir) const override { - if (!HasInfoButton()) { - return true; - } - // We handle these moves ourselves, so MoveFocus won't play the sound for them. - if (dir == UI::FocusMove::RIGHT && !infoSelected_) { - infoSelected_ = true; - UI::PlayUISound(UI::UISound::SELECT); - return false; - } - if (dir == UI::FocusMove::LEFT && infoSelected_) { - infoSelected_ = false; - UI::PlayUISound(UI::UISound::SELECT); - return false; - } - return true; - } - - void DeviceLost() override { - if (video_) { - video_->DeviceLost(); - } - } - - UI::Event OnHoldClick; - UI::Event OnHighlight; - -private: - void TriggerOnHoldClick() { - holdStart_ = 0.0; - UI::EventParams e{}; - e.v = this; - e.s = gamePath_.ToString(); - down_ = false; - OnHoldClick.Trigger(e); - } - void TriggerOnHighlight(UI::FocusFlags focusFlags) { - UI::EventParams e{}; - e.v = this; - e.s = gamePath_.ToString(); - e.a = (u32)focusFlags; - OnHighlight.Trigger(e); - } - - void UpdateVideo(const std::shared_ptr &ginfo, bool wantVideo); - - // The large list has room for an info button on each line, doing the same as a long press. - bool HasInfoButton() const { - return viewMode_ == GameBrowserViewMode::LARGE_LIST && holdEnabled_; - } - Bounds InfoButtonBounds() const { - return Bounds(bounds_.x2() - LARGE_LIST_INFO_SIZE - LARGE_LIST_PAD, bounds_.centerY() - LARGE_LIST_INFO_SIZE * 0.5f, LARGE_LIST_INFO_SIZE, LARGE_LIST_INFO_SIZE); - } - // Returns true if the info button took the input. - bool TouchInfoButton(const TouchInput &input) { - if ((input.flags & TouchInputFlags::MOUSE) && (input.buttons & 1) == 0) { - return false; - } - const bool inside = InfoButtonBounds().Contains(input.x, input.y); - if (input.flags & TouchInputFlags::DOWN) { - infoButtonDown_ = inside; - } - if (!infoButtonDown_) { - return false; - } - if (input.flags & TouchInputFlags::UP) { - infoButtonDown_ = false; - if (inside && !(input.flags & TouchInputFlags::CANCEL)) { - TriggerOnHoldClick(); - } - } - return true; - } - - const GameBrowserViewMode viewMode_; - const 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; - bool infoKeyPressed_ = false; - bool hovering_ = false; - bool infoButtonDown_ = false; - // The info button has the keyboard focus, rather than the game. Changed from CanMoveFocus, hence mutable. - mutable bool infoSelected_ = 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(); - } -} - -// These are the built-in icons used for file types that don't have one (or simply files missing one). -static ImageID ChooseGameIcon(const std::shared_ptr &ginfo) { - if (ginfo->GetFilePath().Type() == PathType::HTTP) { - return ginfo->fileType == IdentifiedFileType::UNKNOWN ? ImageID("I_LINK_OUT_QUESTION") : ImageID("I_LINK_OUT"); - } - - switch (ginfo->fileType) { - case IdentifiedFileType::PSP_ELF: - if (ginfo->region == GameRegion::VSH) { - return ImageID::invalid(); // it has a (preset) icon. - } - // 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::PPSSPP_GE_DUMP: return ImageID("I_DISPLAY"); break; - case IdentifiedFileType::PSX_ISO: - case IdentifiedFileType::PSP_PS1_PBP: return ImageID("I_PSX_ISO"); break; - case IdentifiedFileType::PS2_ISO: return ImageID("I_PS2_ISO"); break; - case IdentifiedFileType::PS3_ISO: return ImageID("I_PS3_ISO"); break; - case IdentifiedFileType::PSP_UMD_VIDEO_ISO: return ImageID("I_UMD_VIDEO_ISO"); break; - case IdentifiedFileType::UNKNOWN_ISO: return ImageID("I_UNKNOWN_ISO"); break; - case IdentifiedFileType::PPSSPP_SAVESTATE: - case IdentifiedFileType::ERROR_IDENTIFYING: - case IdentifiedFileType::UNKNOWN_BIN: return ImageID("I_FILE"); break; - case IdentifiedFileType::ARCHIVE_ZIP: return ImageID("I_ARCHIVE_ZIP"); break; - case IdentifiedFileType::ARCHIVE_7Z: return ImageID("I_ARCHIVE_7Z"); break; - case IdentifiedFileType::ARCHIVE_RAR: return ImageID("I_ARCHIVE_RAR"); break; - case IdentifiedFileType::PSP_PKG: return ImageID("I_FOLDER_UPLOAD"); break; - - case IdentifiedFileType::PSP_ISO: - case IdentifiedFileType::PSP_ISO_NP: - return ImageID("I_APP"); // This will practically always be replaced by a real icon. - - default: - return ImageID::invalid(); - } -} - -// Grid style. Fallback -static void DrawIconWithTextGrid(UIContext &dc, ImageID image, std::string_view text, const Bounds &bounds, const UI::Style &style) { - float tw, th; - dc.MeasureText(dc.GetFontStyle(), g_Config.fGameGridScale, g_Config.fGameGridScale, text, &tw, &th, 0); - dc.PushScissor(bounds); - const FontStyle *fontStyle = GetTextStyle(dc, UI::TextSize::Small); - dc.SetFontStyle(*GetTextStyle(dc, UI::TextSize::Small)); - - int iconSize = image == ImageID("I_UP_DIRECTORY") ? (float)dc.Draw()->GetAtlas()->getImage(image)->h : bounds.h * 0.3f; - float textWidth = 0.0f; - float textHeight = 0; - dc.MeasureTextRect(*fontStyle, 1.0f, 1.0f, text, bounds.w - 10, &textWidth, &textHeight, ALIGN_HCENTER | FLAG_WRAP_TEXT); - - int totalHeight = iconSize + (int)textHeight; - - const float y = std::max(0.0f, bounds.h / 2.0f - totalHeight / 2.0f); - - if (image.isValid()) { - const AtlasImage *img = dc.Draw()->GetAtlas()->getImage(image); - if (img && img->h > 0) { - dc.RebindTexture(); - dc.Draw()->DrawImage(image, bounds.centerX(), bounds.y + y + 2, iconSize / (float)img->h, style.fgColor, ALIGN_TOP | ALIGN_HCENTER); - } - } - - if (image != ImageID("I_UP_DIRECTORY") && image != ImageID("I_PIN") && image != ImageID("I_UNPIN")) { - dc.DrawTextRect(text, bounds.Inset(5, y + iconSize + 4, 5, 2), style.fgColor, ALIGN_HCENTER | FLAG_WRAP_TEXT); - } - dc.SetFontStyle(dc.GetTheme().uiFont); - dc.PopScissor(); -} - -// List, large list -// In the list views, rightText (like a game ID) goes at the right edge, as for a game with a picture. -static void DrawIconWithTextList(UIContext &dc, ImageID image, std::string_view text, const Bounds &bounds, GameBrowserViewMode viewMode, const UI::Style &style, std::string_view rightText = {}) { - float tw, th; - dc.MeasureText(dc.GetFontStyle(), 1.0f, 1.0f, text, &tw, &th, 0); - - const float textX = ListTextX(viewMode); - if (viewMode == GameBrowserViewMode::LARGE_LIST) { - const float scale = g_Config.fGameGridScale; - dc.Draw()->DrawImage(image, bounds.x + LARGE_LIST_LEFT_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, style.fgColor, ALIGN_CENTER); - } - - float tx = textX; - int availableWidth = bounds.w - textX - ListTextRightMargin(viewMode); - if (!rightText.empty()) { - float vw, vh; - dc.MeasureText(dc.GetFontStyle(), 0.7f, 0.7f, rightText, &vw, &vh, 0); - availableWidth -= vw + 20; - dc.SetFontScale(0.7f, 0.7f); - dc.DrawText(rightText, 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; - if (availableWidth < tw) { - tx -= (1.0f + sin(time_now_d() * 1.5f)) * sineWidth; - Bounds tb = bounds; - tb.x = bounds.x + textX; - tb.w = availableWidth; - dc.PushScissor(tb); - } - dc.DrawText(text, bounds.x + tx, bounds.centerY(), style.fgColor, ALIGN_VCENTER | FLAG_WRAP_TEXT); - if (availableWidth < tw) { - dc.PopScissor(); - } -} - -void GameButton::Draw(UIContext &dc) { - // Like the XMB, only the selected game plays its video. Only keyboard/gamepad focus selects, since with - // touch or the mouse, getting a video to play without launching the game was too fiddly. - const bool wantVideo = viewMode_ == GameBrowserViewMode::LARGE_LIST && HasFocus() && 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; - - UI::Style style = dc.GetTheme().itemStyle; - if (down_) { - style = dc.GetTheme().itemDownStyle; - } - - // Some types we just draw a default icon for. - ImageID imageIcon = ChooseGameIcon(ginfo); - if (ginfo->Ready(GameInfoFlags::ICON)) { - if (ginfo->icon.texture) { - 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. - // Maybe we should show something for other file types too. - imageIcon = ImageID("I_LINK_OUT_QUESTION"); - } - - int x = bounds_.x; - int y = bounds_.y; - int w = viewMode_ == GameBrowserViewMode::GRID ? bounds_.w : 144; - int h = bounds_.h; - if (viewMode_ == GameBrowserViewMode::LARGE_LIST) { - x += LARGE_LIST_LEFT_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_); - - const bool rowFocused = HasFocus() && !infoSelected_; - if (viewMode_ != GameBrowserViewMode::GRID || !texture) { - if (rowFocused) - style = down_ ? dc.GetTheme().itemDownStyle : dc.GetTheme().itemFocusedStyle; - - Drawable bg = style.background; - - dc.Draw()->Flush(); - dc.RebindTexture(); - dc.FillRect(bg, bounds_); - 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)); - float tw = texture->Width(); - float th = texture->Height(); - - // Adjust position so we don't stretch the image vertically or horizontally. - // Make sure it's not wider than 144 (like Doom Legacy homebrew), ugly in the grid mode. - float nw = std::min(h * tw / th, (float)w); - x += (w - nw) / 2.0f; - w = nw; - } - - int txOffset = down_ ? 4 : 0; - if (viewMode_ != GameBrowserViewMode::GRID) { - txOffset = 0; - } - - Bounds overlayBounds = bounds_; - - // Render button - int dropsize = 10; - if (texture) { - if (viewMode_ == GameBrowserViewMode::LIST) { - x += 4; - } - if (txOffset) { - dropsize = 3; - y += txOffset * 2; - overlayBounds.y += txOffset * 2; - } - if (HasFocus()) { - dc.Draw()->Flush(); - dc.RebindTexture(); - float pulse = sin(time_now_d() * 7.0) * 0.25 + 0.8; - dc.Draw()->DrawImage4Grid(dc.GetTheme().dropShadow4Grid, x - dropsize * 1.5f, y - dropsize * 1.5f, x + w + dropsize * 1.5f, y + h + dropsize * 1.5f, alphaMul(color, pulse), 1.0f); - dc.Draw()->Flush(); - } else { - dc.Draw()->Flush(); - dc.RebindTexture(); - dc.Draw()->DrawImage4Grid(dc.GetTheme().dropShadow4Grid, x - dropsize, y - dropsize * 0.5f, x + w + dropsize, y + h + dropsize * 1.5, alphaMul(shadowColor, 0.5f), 1.0f); - dc.Draw()->Flush(); - } - - dc.Draw()->Flush(); - dc.GetDrawContext()->BindTexture(0, texture); - dc.Draw()->DrawTexRect(x, y, x + w, y + h, 0, 0, 1, 1, color); - dc.Draw()->Flush(); - } - - if (viewMode_ == GameBrowserViewMode::GRID && g_Config.bShowIDOnGameIcon && ginfo->fileType != IdentifiedFileType::PSP_ELF && !ginfo->id_version.empty()) { - std::string_view idStr = ginfo->id_version; - if (ginfo->fileType == IdentifiedFileType::PSP_SAVEDATA_DIRECTORY) { - auto ga = GetI18NCategory(I18NCat::GAME); - idStr = ga->T("SaveData"); - } - dc.SetFontScale(0.5f * g_Config.fGameGridScale, 0.5f * g_Config.fGameGridScale); - dc.DrawText(idStr, bounds_.x + 5, y + 1, 0xFF000000, ALIGN_TOPLEFT); - dc.DrawText(idStr, bounds_.x + 4, y, dc.GetTheme().infoStyle.fgColor, ALIGN_TOPLEFT); - dc.SetFontScale(1.0f, 1.0f); - } - - u32 overlayColor = 0; - if (holdEnabled_ && holdStart_ != 0.0) { - double time_held = time_now_d() - holdStart_; - overlayColor = whiteAlpha(time_held / 2.5f); - if (holdStart_ != 0.0) { - double time_held = time_now_d() - holdStart_; - int holdFrameCount = (int)(time_held * 60.0f); - if (holdFrameCount > 60) { - // Blink before launching by holding - if (((holdFrameCount >> 3) & 1) == 0) - overlayColor = 0x0; - } - } - } - - if (imageIcon.isValid()) { - Style style = dc.GetTheme().itemStyle; - - if (rowFocused) style = dc.GetTheme().itemFocusedStyle; - if (down_) style = dc.GetTheme().itemDownStyle; - if (!IsEnabled()) style = dc.GetTheme().itemDisabledStyle; - - if (viewMode_ == GameBrowserViewMode::GRID) { - DrawIconWithTextGrid(dc, imageIcon, ginfo->GetTitle(), bounds_, style); - } else { - DrawIconWithTextList(dc, imageIcon, ginfo->GetTitle(), bounds_, viewMode_, style, g_Config.bShowIDOnGameIcon ? std::string_view(ginfo->id_version) : std::string_view()); - } - } else { - char discNumInfo[8]; - if (ginfo->disc_total > 1) - snprintf(discNumInfo, sizeof(discNumInfo), "-DISC%d", ginfo->disc_number); - else - discNumInfo[0] = '\0'; - - dc.Draw()->Flush(); - dc.RebindTexture(); - dc.SetFontStyle(dc.GetTheme().uiFont); - if (viewMode_ != GameBrowserViewMode::GRID) { - float tw, th; - dc.Draw()->Flush(); - dc.PushScissor(bounds_); - const std::string currentTitle = ginfo->GetTitle(); - if (!currentTitle.empty()) { - title_ = ReplaceAll(currentTitle, "\n", " "); - } - - dc.MeasureText(dc.GetFontStyle(), 1.0f, 1.0f, title_, &tw, &th, 0); - - int availableWidth = bounds_.w - textX - ListTextRightMargin(viewMode_); - 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 + 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 = textX; - if (availableWidth < tw) { - tx -= (1.0f + sin(time_now_d() * 1.5f)) * sineWidth; - Bounds tb = bounds_; - tb.x = bounds_.x + textX; - tb.w = availableWidth; - dc.PushScissor(tb); - } - dc.DrawText(title_, bounds_.x + tx, bounds_.centerY(), style.fgColor, ALIGN_VCENTER); - if (availableWidth < tw) { - dc.PopScissor(); - } - dc.Draw()->Flush(); - dc.PopScissor(); - } else if (!texture) { - dc.Draw()->Flush(); - dc.PushScissor(bounds_); - dc.DrawText(title_, bounds_.x + 4, bounds_.centerY(), style.fgColor, ALIGN_VCENTER); - dc.Draw()->Flush(); - dc.PopScissor(); - } else { - dc.Draw()->Flush(); - } - } - - if (ginfo->hasConfig && !ginfo->id.empty()) { - const AtlasImage *gearImage = dc.Draw()->GetAtlas()->getImage(ImageID("I_GEAR_SMALL")); - if (gearImage) { - if (viewMode_ == GameBrowserViewMode::GRID) { - dc.Draw()->DrawImage(ImageID("I_GEAR_SMALL"), bounds_.x, y + h - gearImage->h * g_Config.fGameGridScale, g_Config.fGameGridScale); - } else if (viewMode_ == GameBrowserViewMode::LARGE_LIST) { - dc.Draw()->DrawImage(ImageID("I_GEAR_SMALL"), bounds_.x + LARGE_LIST_LEFT_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); - } - } - } - - const int regionIndex = (int)ginfo->region; - if (g_Config.bShowRegionOnGameIcon && regionIndex >= 0 && regionIndex < (int)GameRegion::COUNT) { - const ImageID regionIcons[(int)GameRegion::COUNT] = { - ImageID("I_FLAG_JP"), - ImageID("I_FLAG_US"), - ImageID("I_FLAG_EU"), - ImageID("I_FLAG_HK"), - ImageID("I_FLAG_AS"), - ImageID("I_FLAG_KO"), - }; - const AtlasImage *regionImage = dc.Draw()->GetAtlas()->getImage(regionIcons[regionIndex]); - if (regionImage) { - if (viewMode_ == GameBrowserViewMode::GRID) { - dc.Draw()->DrawImage(regionIcons[regionIndex], bounds_.x + bounds_.w - (regionImage->w + 5) * g_Config.fGameGridScale, - y + h - (regionImage->h + 5) * g_Config.fGameGridScale, g_Config.fGameGridScale); - } else if (viewMode_ == GameBrowserViewMode::LARGE_LIST) { - dc.Draw()->DrawImage(regionIcons[regionIndex], bounds_.x + LARGE_LIST_LEFT_PAD + (LARGE_LIST_ICON_W - regionImage->w - 5) * g_Config.fGameGridScale, - y + h - (regionImage->h + 5) * g_Config.fGameGridScale, g_Config.fGameGridScale); - } else { - dc.Draw()->DrawImage(regionIcons[regionIndex], bounds_.x + 4, y + h - regionImage->h - 5, 1.0f); - } - } - } - - if (HasInfoButton()) { - const Bounds infoBounds = InfoButtonBounds(); - Style infoStyle = HasFocus() ? dc.GetTheme().itemFocusedStyle : dc.GetTheme().itemStyle; - if (infoButtonDown_) { - infoStyle = dc.GetTheme().itemDownStyle; - dc.FillRoundRect(infoStyle.background, infoBounds, dc.GetTheme().choiceRoundRadius); - } else if (HasFocus() && infoSelected_) { - dc.FillRoundRect(infoStyle.background, infoBounds, dc.GetTheme().choiceRoundRadius); - } - dc.Draw()->DrawImage(ImageID("I_INFO"), infoBounds.centerX(), infoBounds.centerY(), 1.0f, infoStyle.fgColor, ALIGN_CENTER); - } - - if (overlayColor) { - dc.FillRect(Drawable(overlayColor), overlayBounds); - } - - // Covers the icon while the game is being compressed. Clicking opens the game screen instead of playing. - GamePacker::Status packStatus; - if (GamePacker::IsPacking(gamePath_) && GamePacker::GetStatus(&packStatus)) { - auto ga = GetI18NCategory(I18NCat::GAME); - const Bounds iconBounds(x, y, w, h); - dc.FillRect(Drawable(0xD0505050), iconBounds); - const float scale = viewMode_ == GameBrowserViewMode::GRID || viewMode_ == GameBrowserViewMode::LARGE_LIST ? 0.8f * g_Config.fGameGridScale : 0.6f; - dc.SetFontStyle(dc.GetTheme().uiFont); - dc.SetFontScale(scale, scale); - const std::string text = std::string(ga->T("Compressing...")) + StringFromFormat("\n%d%%", (int)(packStatus.progress * 100.0f)); - dc.DrawTextRectSqueeze(text, iconBounds, 0xFFFFFFFF, ALIGN_CENTER); - dc.SetFontScale(1.0f, 1.0f); - } - - dc.RebindTexture(); -} - -std::string GameButton::DescribeText() const { - // LOW: search calls this for every game in the list, which mustn't hold up the visible ones. - std::shared_ptr ginfo = g_gameInfoCache->GetInfo(nullptr, gamePath_, GameInfoFlags::PARAM_SFO, nullptr, GameInfoFlags::EMPTY, TaskPriority::LOW); - if (!ginfo->Ready(GameInfoFlags::PARAM_SFO)) - return "..."; - auto u = GetI18NCategory(I18NCat::UI_ELEMENTS); - return ginfo->GetTitle(); -} - class DirButton : public UI::Button { public: DirButton(const Path &path, GameBrowserViewMode viewMode, UI::LayoutParams *layoutParams) diff --git a/UI/GameButton.cpp b/UI/GameButton.cpp new file mode 100644 index 0000000000..9f86550e69 --- /dev/null +++ b/UI/GameButton.cpp @@ -0,0 +1,471 @@ +// Copyright (c) 2012- PPSSPP Project. + +// This program is free software: you can redistribute it and/or modify +// it under the terms of the GNU General Public License as published by +// the Free Software Foundation, version 2.0 or later versions. + +// This program is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU General Public License 2.0 for more details. + +// A copy of the GPL 2.0 should have been included with the program. +// If not, see http://www.gnu.org/licenses/ + +// Official git repository and contact information can be found at +// https://github.com/hrydgard/ppsspp and http://www.ppsspp.org/. + + +#include +#include + +#include "Common/Data/Text/I18n.h" +#include "Common/Math/curves.h" +#include "Common/StringUtils.h" +#include "Common/TimeUtil.h" +#include "Common/UI/Context.h" +#include "Core/Config.h" +#include "Core/Loaders.h" +#include "Core/Util/VideoPlayer.h" +#include "UI/GameButton.h" +#include "UI/GameInfoCache.h" +#include "UI/GamePacker.h" + + +// Where the text starts in the list modes. +static float ListTextX(GameBrowserViewMode viewMode) { + if (viewMode == GameBrowserViewMode::LARGE_LIST) { + return LARGE_LIST_LEFT_PAD + LARGE_LIST_ICON_W * g_Config.fGameGridScale + 20.0f; + } + return 150.0f; +} + +// Space to the right of the text in the list modes, for the info button. +static float ListTextRightMargin(GameBrowserViewMode viewMode) { + if (viewMode == GameBrowserViewMode::LARGE_LIST) { + return LARGE_LIST_INFO_SIZE + LARGE_LIST_PAD * 2; + } + return 0.0f; +} + + +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(); + } +} + +// These are the built-in icons used for file types that don't have one (or simply files missing one). +static ImageID ChooseGameIcon(const std::shared_ptr &ginfo) { + if (ginfo->GetFilePath().Type() == PathType::HTTP) { + return ginfo->fileType == IdentifiedFileType::UNKNOWN ? ImageID("I_LINK_OUT_QUESTION") : ImageID("I_LINK_OUT"); + } + + switch (ginfo->fileType) { + case IdentifiedFileType::PSP_ELF: + if (ginfo->region == GameRegion::VSH) { + return ImageID::invalid(); // it has a (preset) icon. + } + // 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::PPSSPP_GE_DUMP: return ImageID("I_DISPLAY"); break; + case IdentifiedFileType::PSX_ISO: + case IdentifiedFileType::PSP_PS1_PBP: return ImageID("I_PSX_ISO"); break; + case IdentifiedFileType::PS2_ISO: return ImageID("I_PS2_ISO"); break; + case IdentifiedFileType::PS3_ISO: return ImageID("I_PS3_ISO"); break; + case IdentifiedFileType::PSP_UMD_VIDEO_ISO: return ImageID("I_UMD_VIDEO_ISO"); break; + case IdentifiedFileType::UNKNOWN_ISO: return ImageID("I_UNKNOWN_ISO"); break; + case IdentifiedFileType::PPSSPP_SAVESTATE: + case IdentifiedFileType::ERROR_IDENTIFYING: + case IdentifiedFileType::UNKNOWN_BIN: return ImageID("I_FILE"); break; + case IdentifiedFileType::ARCHIVE_ZIP: return ImageID("I_ARCHIVE_ZIP"); break; + case IdentifiedFileType::ARCHIVE_7Z: return ImageID("I_ARCHIVE_7Z"); break; + case IdentifiedFileType::ARCHIVE_RAR: return ImageID("I_ARCHIVE_RAR"); break; + case IdentifiedFileType::PSP_PKG: return ImageID("I_FOLDER_UPLOAD"); break; + + case IdentifiedFileType::PSP_ISO: + case IdentifiedFileType::PSP_ISO_NP: + return ImageID("I_APP"); // This will practically always be replaced by a real icon. + + default: + return ImageID::invalid(); + } +} + +// Grid style. Fallback +void DrawIconWithTextGrid(UIContext &dc, ImageID image, std::string_view text, const Bounds &bounds, const UI::Style &style) { + float tw, th; + dc.MeasureText(dc.GetFontStyle(), g_Config.fGameGridScale, g_Config.fGameGridScale, text, &tw, &th, 0); + dc.PushScissor(bounds); + const FontStyle *fontStyle = GetTextStyle(dc, UI::TextSize::Small); + dc.SetFontStyle(*GetTextStyle(dc, UI::TextSize::Small)); + + int iconSize = image == ImageID("I_UP_DIRECTORY") ? (float)dc.Draw()->GetAtlas()->getImage(image)->h : bounds.h * 0.3f; + float textWidth = 0.0f; + float textHeight = 0; + dc.MeasureTextRect(*fontStyle, 1.0f, 1.0f, text, bounds.w - 10, &textWidth, &textHeight, ALIGN_HCENTER | FLAG_WRAP_TEXT); + + int totalHeight = iconSize + (int)textHeight; + + const float y = std::max(0.0f, bounds.h / 2.0f - totalHeight / 2.0f); + + if (image.isValid()) { + const AtlasImage *img = dc.Draw()->GetAtlas()->getImage(image); + if (img && img->h > 0) { + dc.RebindTexture(); + dc.Draw()->DrawImage(image, bounds.centerX(), bounds.y + y + 2, iconSize / (float)img->h, style.fgColor, ALIGN_TOP | ALIGN_HCENTER); + } + } + + if (image != ImageID("I_UP_DIRECTORY") && image != ImageID("I_PIN") && image != ImageID("I_UNPIN")) { + dc.DrawTextRect(text, bounds.Inset(5, y + iconSize + 4, 5, 2), style.fgColor, ALIGN_HCENTER | FLAG_WRAP_TEXT); + } + dc.SetFontStyle(dc.GetTheme().uiFont); + dc.PopScissor(); +} + +// List, large list +// In the list views, rightText (like a game ID) goes at the right edge, as for a game with a picture. +void DrawIconWithTextList(UIContext &dc, ImageID image, std::string_view text, const Bounds &bounds, GameBrowserViewMode viewMode, const UI::Style &style, std::string_view rightText) { + float tw, th; + dc.MeasureText(dc.GetFontStyle(), 1.0f, 1.0f, text, &tw, &th, 0); + + const float textX = ListTextX(viewMode); + if (viewMode == GameBrowserViewMode::LARGE_LIST) { + const float scale = g_Config.fGameGridScale; + dc.Draw()->DrawImage(image, bounds.x + LARGE_LIST_LEFT_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, style.fgColor, ALIGN_CENTER); + } + + float tx = textX; + int availableWidth = bounds.w - textX - ListTextRightMargin(viewMode); + if (!rightText.empty()) { + float vw, vh; + dc.MeasureText(dc.GetFontStyle(), 0.7f, 0.7f, rightText, &vw, &vh, 0); + availableWidth -= vw + 20; + dc.SetFontScale(0.7f, 0.7f); + dc.DrawText(rightText, 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; + if (availableWidth < tw) { + tx -= (1.0f + sin(time_now_d() * 1.5f)) * sineWidth; + Bounds tb = bounds; + tb.x = bounds.x + textX; + tb.w = availableWidth; + dc.PushScissor(tb); + } + dc.DrawText(text, bounds.x + tx, bounds.centerY(), style.fgColor, ALIGN_VCENTER | FLAG_WRAP_TEXT); + if (availableWidth < tw) { + dc.PopScissor(); + } +} + +void GameButton::Draw(UIContext &dc) { + // Like the XMB, only the selected game plays its video. Only keyboard/gamepad focus selects, since with + // touch or the mouse, getting a video to play without launching the game was too fiddly. + const bool wantVideo = viewMode_ == GameBrowserViewMode::LARGE_LIST && HasFocus() && 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; + + UI::Style style = dc.GetTheme().itemStyle; + if (down_) { + style = dc.GetTheme().itemDownStyle; + } + + // Some types we just draw a default icon for. + ImageID imageIcon = ChooseGameIcon(ginfo); + if (ginfo->Ready(GameInfoFlags::ICON)) { + if (ginfo->icon.texture) { + 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. + // Maybe we should show something for other file types too. + imageIcon = ImageID("I_LINK_OUT_QUESTION"); + } + + int x = bounds_.x; + int y = bounds_.y; + int w = viewMode_ == GameBrowserViewMode::GRID ? bounds_.w : 144; + int h = bounds_.h; + if (viewMode_ == GameBrowserViewMode::LARGE_LIST) { + x += LARGE_LIST_LEFT_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_); + + const bool rowFocused = HasFocus() && !infoSelected_; + if (viewMode_ != GameBrowserViewMode::GRID || !texture) { + if (rowFocused) + style = down_ ? dc.GetTheme().itemDownStyle : dc.GetTheme().itemFocusedStyle; + + Drawable bg = style.background; + + dc.Draw()->Flush(); + dc.RebindTexture(); + dc.FillRect(bg, bounds_); + 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)); + float tw = texture->Width(); + float th = texture->Height(); + + // Adjust position so we don't stretch the image vertically or horizontally. + // Make sure it's not wider than 144 (like Doom Legacy homebrew), ugly in the grid mode. + float nw = std::min(h * tw / th, (float)w); + x += (w - nw) / 2.0f; + w = nw; + } + + int txOffset = down_ ? 4 : 0; + if (viewMode_ != GameBrowserViewMode::GRID) { + txOffset = 0; + } + + Bounds overlayBounds = bounds_; + + // Render button + int dropsize = 10; + if (texture) { + if (viewMode_ == GameBrowserViewMode::LIST) { + x += 4; + } + if (txOffset) { + dropsize = 3; + y += txOffset * 2; + overlayBounds.y += txOffset * 2; + } + if (HasFocus()) { + dc.Draw()->Flush(); + dc.RebindTexture(); + float pulse = sin(time_now_d() * 7.0) * 0.25 + 0.8; + dc.Draw()->DrawImage4Grid(dc.GetTheme().dropShadow4Grid, x - dropsize * 1.5f, y - dropsize * 1.5f, x + w + dropsize * 1.5f, y + h + dropsize * 1.5f, alphaMul(color, pulse), 1.0f); + dc.Draw()->Flush(); + } else { + dc.Draw()->Flush(); + dc.RebindTexture(); + dc.Draw()->DrawImage4Grid(dc.GetTheme().dropShadow4Grid, x - dropsize, y - dropsize * 0.5f, x + w + dropsize, y + h + dropsize * 1.5, alphaMul(shadowColor, 0.5f), 1.0f); + dc.Draw()->Flush(); + } + + dc.Draw()->Flush(); + dc.GetDrawContext()->BindTexture(0, texture); + dc.Draw()->DrawTexRect(x, y, x + w, y + h, 0, 0, 1, 1, color); + dc.Draw()->Flush(); + } + + if (viewMode_ == GameBrowserViewMode::GRID && g_Config.bShowIDOnGameIcon && ginfo->fileType != IdentifiedFileType::PSP_ELF && !ginfo->id_version.empty()) { + std::string_view idStr = ginfo->id_version; + if (ginfo->fileType == IdentifiedFileType::PSP_SAVEDATA_DIRECTORY) { + auto ga = GetI18NCategory(I18NCat::GAME); + idStr = ga->T("SaveData"); + } + dc.SetFontScale(0.5f * g_Config.fGameGridScale, 0.5f * g_Config.fGameGridScale); + dc.DrawText(idStr, bounds_.x + 5, y + 1, 0xFF000000, ALIGN_TOPLEFT); + dc.DrawText(idStr, bounds_.x + 4, y, dc.GetTheme().infoStyle.fgColor, ALIGN_TOPLEFT); + dc.SetFontScale(1.0f, 1.0f); + } + + u32 overlayColor = 0; + if (holdEnabled_ && holdStart_ != 0.0) { + double time_held = time_now_d() - holdStart_; + overlayColor = whiteAlpha(time_held / 2.5f); + if (holdStart_ != 0.0) { + double time_held = time_now_d() - holdStart_; + int holdFrameCount = (int)(time_held * 60.0f); + if (holdFrameCount > 60) { + // Blink before launching by holding + if (((holdFrameCount >> 3) & 1) == 0) + overlayColor = 0x0; + } + } + } + + if (imageIcon.isValid()) { + Style style = dc.GetTheme().itemStyle; + + if (rowFocused) style = dc.GetTheme().itemFocusedStyle; + if (down_) style = dc.GetTheme().itemDownStyle; + if (!IsEnabled()) style = dc.GetTheme().itemDisabledStyle; + + if (viewMode_ == GameBrowserViewMode::GRID) { + DrawIconWithTextGrid(dc, imageIcon, ginfo->GetTitle(), bounds_, style); + } else { + DrawIconWithTextList(dc, imageIcon, ginfo->GetTitle(), bounds_, viewMode_, style, g_Config.bShowIDOnGameIcon ? std::string_view(ginfo->id_version) : std::string_view()); + } + } else { + char discNumInfo[8]; + if (ginfo->disc_total > 1) + snprintf(discNumInfo, sizeof(discNumInfo), "-DISC%d", ginfo->disc_number); + else + discNumInfo[0] = '\0'; + + dc.Draw()->Flush(); + dc.RebindTexture(); + dc.SetFontStyle(dc.GetTheme().uiFont); + if (viewMode_ != GameBrowserViewMode::GRID) { + float tw, th; + dc.Draw()->Flush(); + dc.PushScissor(bounds_); + const std::string currentTitle = ginfo->GetTitle(); + if (!currentTitle.empty()) { + title_ = ReplaceAll(currentTitle, "\n", " "); + } + + dc.MeasureText(dc.GetFontStyle(), 1.0f, 1.0f, title_, &tw, &th, 0); + + int availableWidth = bounds_.w - textX - ListTextRightMargin(viewMode_); + 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 + 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 = textX; + if (availableWidth < tw) { + tx -= (1.0f + sin(time_now_d() * 1.5f)) * sineWidth; + Bounds tb = bounds_; + tb.x = bounds_.x + textX; + tb.w = availableWidth; + dc.PushScissor(tb); + } + dc.DrawText(title_, bounds_.x + tx, bounds_.centerY(), style.fgColor, ALIGN_VCENTER); + if (availableWidth < tw) { + dc.PopScissor(); + } + dc.Draw()->Flush(); + dc.PopScissor(); + } else if (!texture) { + dc.Draw()->Flush(); + dc.PushScissor(bounds_); + dc.DrawText(title_, bounds_.x + 4, bounds_.centerY(), style.fgColor, ALIGN_VCENTER); + dc.Draw()->Flush(); + dc.PopScissor(); + } else { + dc.Draw()->Flush(); + } + } + + if (ginfo->hasConfig && !ginfo->id.empty()) { + const AtlasImage *gearImage = dc.Draw()->GetAtlas()->getImage(ImageID("I_GEAR_SMALL")); + if (gearImage) { + if (viewMode_ == GameBrowserViewMode::GRID) { + dc.Draw()->DrawImage(ImageID("I_GEAR_SMALL"), bounds_.x, y + h - gearImage->h * g_Config.fGameGridScale, g_Config.fGameGridScale); + } else if (viewMode_ == GameBrowserViewMode::LARGE_LIST) { + dc.Draw()->DrawImage(ImageID("I_GEAR_SMALL"), bounds_.x + LARGE_LIST_LEFT_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); + } + } + } + + const int regionIndex = (int)ginfo->region; + if (g_Config.bShowRegionOnGameIcon && regionIndex >= 0 && regionIndex < (int)GameRegion::COUNT) { + const ImageID regionIcons[(int)GameRegion::COUNT] = { + ImageID("I_FLAG_JP"), + ImageID("I_FLAG_US"), + ImageID("I_FLAG_EU"), + ImageID("I_FLAG_HK"), + ImageID("I_FLAG_AS"), + ImageID("I_FLAG_KO"), + }; + const AtlasImage *regionImage = dc.Draw()->GetAtlas()->getImage(regionIcons[regionIndex]); + if (regionImage) { + if (viewMode_ == GameBrowserViewMode::GRID) { + dc.Draw()->DrawImage(regionIcons[regionIndex], bounds_.x + bounds_.w - (regionImage->w + 5) * g_Config.fGameGridScale, + y + h - (regionImage->h + 5) * g_Config.fGameGridScale, g_Config.fGameGridScale); + } else if (viewMode_ == GameBrowserViewMode::LARGE_LIST) { + dc.Draw()->DrawImage(regionIcons[regionIndex], bounds_.x + LARGE_LIST_LEFT_PAD + (LARGE_LIST_ICON_W - regionImage->w - 5) * g_Config.fGameGridScale, + y + h - (regionImage->h + 5) * g_Config.fGameGridScale, g_Config.fGameGridScale); + } else { + dc.Draw()->DrawImage(regionIcons[regionIndex], bounds_.x + 4, y + h - regionImage->h - 5, 1.0f); + } + } + } + + if (HasInfoButton()) { + const Bounds infoBounds = InfoButtonBounds(); + Style infoStyle = HasFocus() ? dc.GetTheme().itemFocusedStyle : dc.GetTheme().itemStyle; + if (infoButtonDown_) { + infoStyle = dc.GetTheme().itemDownStyle; + dc.FillRoundRect(infoStyle.background, infoBounds, dc.GetTheme().choiceRoundRadius); + } else if (HasFocus() && infoSelected_) { + dc.FillRoundRect(infoStyle.background, infoBounds, dc.GetTheme().choiceRoundRadius); + } + dc.Draw()->DrawImage(ImageID("I_INFO"), infoBounds.centerX(), infoBounds.centerY(), 1.0f, infoStyle.fgColor, ALIGN_CENTER); + } + + if (overlayColor) { + dc.FillRect(Drawable(overlayColor), overlayBounds); + } + + // Covers the icon while the game is being compressed. Clicking opens the game screen instead of playing. + GamePacker::Status packStatus; + if (GamePacker::IsPacking(gamePath_) && GamePacker::GetStatus(&packStatus)) { + auto ga = GetI18NCategory(I18NCat::GAME); + const Bounds iconBounds(x, y, w, h); + dc.FillRect(Drawable(0xD0505050), iconBounds); + const float scale = viewMode_ == GameBrowserViewMode::GRID || viewMode_ == GameBrowserViewMode::LARGE_LIST ? 0.8f * g_Config.fGameGridScale : 0.6f; + dc.SetFontStyle(dc.GetTheme().uiFont); + dc.SetFontScale(scale, scale); + const std::string text = std::string(ga->T("Compressing...")) + StringFromFormat("\n%d%%", (int)(packStatus.progress * 100.0f)); + dc.DrawTextRectSqueeze(text, iconBounds, 0xFFFFFFFF, ALIGN_CENTER); + dc.SetFontScale(1.0f, 1.0f); + } + + dc.RebindTexture(); +} + +std::string GameButton::DescribeText() const { + // LOW: search calls this for every game in the list, which mustn't hold up the visible ones. + std::shared_ptr ginfo = g_gameInfoCache->GetInfo(nullptr, gamePath_, GameInfoFlags::PARAM_SFO, nullptr, GameInfoFlags::EMPTY, TaskPriority::LOW); + if (!ginfo->Ready(GameInfoFlags::PARAM_SFO)) + return "..."; + auto u = GetI18NCategory(I18NCat::UI_ELEMENTS); + return ginfo->GetTitle(); +} diff --git a/UI/GameButton.h b/UI/GameButton.h new file mode 100644 index 0000000000..5d222b1db8 --- /dev/null +++ b/UI/GameButton.h @@ -0,0 +1,262 @@ +// Copyright (c) 2012- PPSSPP Project. + +// This program is free software: you can redistribute it and/or modify +// it under the terms of the GNU General Public License as published by +// the Free Software Foundation, version 2.0 or later versions. + +// This program is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU General Public License 2.0 for more details. + +// A copy of the GPL 2.0 should have been included with the program. +// If not, see http://www.gnu.org/licenses/ + +// Official git repository and contact information can be found at +// https://github.com/hrydgard/ppsspp and http://www.ppsspp.org/. + + +#pragma once + +#include +#include +#include +#include + +#include "Common/File/Path.h" +#include "Common/TimeUtil.h" +#include "Common/UI/View.h" +#include "Common/UI/Root.h" +#include "Core/Config.h" +#include "Core/ConfigValues.h" +#include "UI/MiscViews.h" + +class GameInfo; + +// 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; +static constexpr float LARGE_LIST_LEFT_PAD = 11.0f; +static constexpr float LARGE_LIST_INFO_SIZE = 64.0f; + +class GameButton : public UI::Clickable { +public: + GameButton(const Path &gamePath, GameBrowserViewMode viewMode, UI::LayoutParams *layoutParams = nullptr) + : UI::Clickable(layoutParams), viewMode_(viewMode), gamePath_(gamePath) {} + + void Draw(UIContext &dc) override; + std::string DescribeText() const override; + std::string SearchAlias() const override { + // Without the extension, or searching for "iso" would match every ISO. Only three or more + // letters counts as one, so a folder like "Game v1.2" keeps its version number. + std::string filename = gamePath_.GetFilename(); + const std::string ext = gamePath_.GetFileExtension(); + if (ext.size() >= 4 && ext.size() <= filename.size() && std::all_of(ext.begin() + 1, ext.end(), [](char c) { return c >= 'a' && c <= 'z'; })) { + filename.resize(filename.size() - ext.size()); + } + return filename; + } + void GetContentDimensions(const UIContext &dc, float &w, float &h) const override { + switch (viewMode_) { + case GameBrowserViewMode::GRID: + w = 144 * g_Config.fGameGridScale; + h = 80 * g_Config.fGameGridScale; + break; + case GameBrowserViewMode::LARGE_LIST: + w = 500; + h = LARGE_LIST_ICON_H * g_Config.fGameGridScale + LARGE_LIST_PAD * 2; + break; + default: // List + w = 500; + h = 50; + break; + } + } + const Path &GamePath() const { return gamePath_; } + + void SetHoldEnabled(bool hold) { + holdEnabled_ = hold; + } + bool Touch(const TouchInput &input) override { + hovering_ = bounds_.Contains(input.x, input.y); + if (HasInfoButton() && TouchInfoButton(input)) { + return true; + } + const bool retval = UI::Clickable::Touch(input); + if (hovering_ && (input.flags & TouchInputFlags::DOWN)) { + holdStart_ = time_now_d(); + } + if (input.flags & TouchInputFlags::UP) { + holdStart_ = 0; + } + return retval; + } + + bool Key(const KeyInput &key) override { + bool showInfo = false; + + if (HasFocus() && UI::IsInfoKey(key)) { + // If it's the button that's mapped to triangle, then show the info. + if (key.flags & KeyInputFlags::DOWN) { + infoKeyPressed_ = true; + } + if ((key.flags & KeyInputFlags::UP) && infoKeyPressed_) { + showInfo = true; + infoKeyPressed_ = false; + } + } else if (hovering_ && key.deviceId == DEVICE_ID_MOUSE && key.keyCode == NKCODE_EXT_MOUSEBUTTON_2) { + // If it's the right mouse button, and it's not otherwise mapped, show the info also. + if (key.flags & KeyInputFlags::DOWN) { + showInfoPressed_ = true; + } + if ((key.flags & KeyInputFlags::UP) && showInfoPressed_) { + showInfo = true; + showInfoPressed_ = false; + } + } + + if (showInfo) { + TriggerOnHoldClick(); + return true; + } + + if (infoSelected_ && HasFocus() && UI::IsAcceptKey(key)) { + if (key.flags & KeyInputFlags::DOWN) { + infoButtonDown_ = true; + } + if ((key.flags & KeyInputFlags::UP) && infoButtonDown_) { + infoButtonDown_ = false; + TriggerOnHoldClick(); + } + return true; + } + + // Holding the accept button works like a long press, so a gamepad can do it too. + if (holdEnabled_ && HasFocus() && UI::IsAcceptKey(key) && !(key.flags & KeyInputFlags::IS_REPEAT)) { + if (key.flags & KeyInputFlags::DOWN) { + holdStart_ = time_now_d(); + } + if (key.flags & KeyInputFlags::UP) { + holdStart_ = 0.0; + } + } + + return Clickable::Key(key); + } + + void Update() override { + // Hold button for 1.5 seconds to launch the game options + if (holdEnabled_ && holdStart_ != 0.0 && holdStart_ < time_now_d() - 1.5) { + TriggerOnHoldClick(); + } + } + + void FocusChanged(UI::FocusFlags focusFlags) override { + UI::Clickable::FocusChanged(focusFlags); + if (focusFlags & UI::FocusFlags::LOST_FOCUS) { + // Don't let a held accept button carry over from a button we've left. + holdStart_ = 0.0; + // Moving to another game selects the game, not its info button. + infoSelected_ = false; + infoButtonDown_ = false; + } + TriggerOnHighlight(focusFlags); + } + + // Left and right move between the game and its info button, so up and down still go from game to game. + bool CanMoveFocus(UI::FocusMove dir) const override { + if (!HasInfoButton()) { + return true; + } + // We handle these moves ourselves, so MoveFocus won't play the sound for them. + if (dir == UI::FocusMove::RIGHT && !infoSelected_) { + infoSelected_ = true; + UI::PlayUISound(UI::UISound::SELECT); + return false; + } + if (dir == UI::FocusMove::LEFT && infoSelected_) { + infoSelected_ = false; + UI::PlayUISound(UI::UISound::SELECT); + return false; + } + return true; + } + + void DeviceLost() override { + if (video_) { + video_->DeviceLost(); + } + } + + UI::Event OnHoldClick; + UI::Event OnHighlight; + +private: + void TriggerOnHoldClick() { + holdStart_ = 0.0; + UI::EventParams e{}; + e.v = this; + e.s = gamePath_.ToString(); + down_ = false; + OnHoldClick.Trigger(e); + } + void TriggerOnHighlight(UI::FocusFlags focusFlags) { + UI::EventParams e{}; + e.v = this; + e.s = gamePath_.ToString(); + e.a = (u32)focusFlags; + OnHighlight.Trigger(e); + } + + void UpdateVideo(const std::shared_ptr &ginfo, bool wantVideo); + + // The large list has room for an info button on each line, doing the same as a long press. + bool HasInfoButton() const { + return viewMode_ == GameBrowserViewMode::LARGE_LIST && holdEnabled_; + } + Bounds InfoButtonBounds() const { + return Bounds(bounds_.x2() - LARGE_LIST_INFO_SIZE - LARGE_LIST_PAD, bounds_.centerY() - LARGE_LIST_INFO_SIZE * 0.5f, LARGE_LIST_INFO_SIZE, LARGE_LIST_INFO_SIZE); + } + // Returns true if the info button took the input. + bool TouchInfoButton(const TouchInput &input) { + if ((input.flags & TouchInputFlags::MOUSE) && (input.buttons & 1) == 0) { + return false; + } + const bool inside = InfoButtonBounds().Contains(input.x, input.y); + if (input.flags & TouchInputFlags::DOWN) { + infoButtonDown_ = inside; + } + if (!infoButtonDown_) { + return false; + } + if (input.flags & TouchInputFlags::UP) { + infoButtonDown_ = false; + if (inside && !(input.flags & TouchInputFlags::CANCEL)) { + TriggerOnHoldClick(); + } + } + return true; + } + + const GameBrowserViewMode viewMode_; + const 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; + bool infoKeyPressed_ = false; + bool hovering_ = false; + bool infoButtonDown_ = false; + // The info button has the keyboard focus, rather than the game. Changed from CanMoveFocus, hence mutable. + mutable bool infoSelected_ = false; +}; + +// Also used for the folder buttons. +void DrawIconWithTextGrid(UIContext &dc, ImageID image, std::string_view text, const Bounds &bounds, const UI::Style &style); +void DrawIconWithTextList(UIContext &dc, ImageID image, std::string_view text, const Bounds &bounds, GameBrowserViewMode viewMode, const UI::Style &style, std::string_view rightText = {}); diff --git a/UI/UI.vcxproj b/UI/UI.vcxproj index 052e68b0f0..5160aded30 100644 --- a/UI/UI.vcxproj +++ b/UI/UI.vcxproj @@ -44,6 +44,7 @@ + @@ -118,6 +119,7 @@ + diff --git a/UI/UI.vcxproj.filters b/UI/UI.vcxproj.filters index ea7e73f043..fe27a86b8b 100644 --- a/UI/UI.vcxproj.filters +++ b/UI/UI.vcxproj.filters @@ -196,6 +196,9 @@ Views + + Views + @@ -372,6 +375,9 @@ Views + + Views + diff --git a/UWP/UI_UWP/UI_UWP.vcxproj b/UWP/UI_UWP/UI_UWP.vcxproj index c1bfdc6177..7785a56d7e 100644 --- a/UWP/UI_UWP/UI_UWP.vcxproj +++ b/UWP/UI_UWP/UI_UWP.vcxproj @@ -103,6 +103,7 @@ + @@ -170,6 +171,7 @@ + diff --git a/UWP/UI_UWP/UI_UWP.vcxproj.filters b/UWP/UI_UWP/UI_UWP.vcxproj.filters index ed9ddb9258..ee3797984f 100644 --- a/UWP/UI_UWP/UI_UWP.vcxproj.filters +++ b/UWP/UI_UWP/UI_UWP.vcxproj.filters @@ -188,6 +188,7 @@ Screens + @@ -353,6 +354,7 @@ Screens + diff --git a/android/jni/Android.mk b/android/jni/Android.mk index b356ab1045..3d171600d1 100644 --- a/android/jni/Android.mk +++ b/android/jni/Android.mk @@ -1066,6 +1066,7 @@ LOCAL_SRC_FILES := \ $(SRC)/UI/EmuScreen.cpp \ $(SRC)/UI/ImposeHome.cpp \ $(SRC)/UI/GameBrowser.cpp \ + $(SRC)/UI/GameButton.cpp \ $(SRC)/UI/MainScreen.cpp \ $(SRC)/UI/TabbedDialogScreen.cpp \ $(SRC)/UI/SettingsPage.cpp \ From 3f5a79bfbc6b7edac15efcaf01c684bd438bb494 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Henrik=20Rydg=C3=A5rd?= Date: Sun, 1 Nov 2026 00:29:52 +0100 Subject: [PATCH 5/8] Simplify --- Common/Render/DrawBuffer.h | 3 ++ UI/GameButton.cpp | 79 +++++++++++++++++++------------------- 2 files changed, 42 insertions(+), 40 deletions(-) diff --git a/Common/Render/DrawBuffer.h b/Common/Render/DrawBuffer.h index a5ffbf4c4e..cacabff8ae 100644 --- a/Common/Render/DrawBuffer.h +++ b/Common/Render/DrawBuffer.h @@ -133,6 +133,9 @@ public: } // Results in 18 triangles. Kind of expensive for a button. void DrawImage4Grid(ImageID atlas_image, float x1, float y1, float x2, float y2, Color color = COLOR(0xFFFFFF), float corner_scale = 1.0); + void DrawImage4Grid(ImageID atlas_image, const Bounds &bounds, Color color = COLOR(0xFFFFFF), float corner_scale = 1.0) { + DrawImage4Grid(atlas_image, bounds.x, bounds.y, bounds.x2(), bounds.y2(), color, corner_scale); + } // Uses the nearest precomputed corner radius, scaled to fit, so outlines get thinner or thicker // when radius isn't one of ROUND_RECT_RADII. 18 triangles filled, 16 outlined. diff --git a/UI/GameButton.cpp b/UI/GameButton.cpp index 9f86550e69..e93773c06c 100644 --- a/UI/GameButton.cpp +++ b/UI/GameButton.cpp @@ -213,15 +213,13 @@ void GameButton::Draw(UIContext &dc) { imageIcon = ImageID("I_LINK_OUT_QUESTION"); } - int x = bounds_.x; - int y = bounds_.y; - int w = viewMode_ == GameBrowserViewMode::GRID ? bounds_.w : 144; - int h = bounds_.h; + Bounds iconBounds = bounds_; + iconBounds.w = viewMode_ == GameBrowserViewMode::GRID ? bounds_.w : 144; if (viewMode_ == GameBrowserViewMode::LARGE_LIST) { - x += LARGE_LIST_LEFT_PAD; - y += LARGE_LIST_PAD; - w = LARGE_LIST_ICON_W * g_Config.fGameGridScale; - h = LARGE_LIST_ICON_H * g_Config.fGameGridScale; + iconBounds.x += LARGE_LIST_LEFT_PAD; + iconBounds.y += LARGE_LIST_PAD; + iconBounds.w = LARGE_LIST_ICON_W * g_Config.fGameGridScale; + iconBounds.h = LARGE_LIST_ICON_H * g_Config.fGameGridScale; } const float textX = ListTextX(viewMode_); @@ -253,9 +251,9 @@ void GameButton::Draw(UIContext &dc) { // Adjust position so we don't stretch the image vertically or horizontally. // Make sure it's not wider than 144 (like Doom Legacy homebrew), ugly in the grid mode. - float nw = std::min(h * tw / th, (float)w); - x += (w - nw) / 2.0f; - w = nw; + float nw = std::min(iconBounds.h * tw / th, (float)iconBounds.w); + iconBounds.x += (iconBounds.w - nw) / 2.0f; + iconBounds.w = nw; } int txOffset = down_ ? 4 : 0; @@ -269,29 +267,31 @@ void GameButton::Draw(UIContext &dc) { int dropsize = 10; if (texture) { if (viewMode_ == GameBrowserViewMode::LIST) { - x += 4; + iconBounds.x += 4; } if (txOffset) { dropsize = 3; - y += txOffset * 2; + iconBounds.y += txOffset * 2; overlayBounds.y += txOffset * 2; } if (HasFocus()) { dc.Draw()->Flush(); dc.RebindTexture(); float pulse = sin(time_now_d() * 7.0) * 0.25 + 0.8; - dc.Draw()->DrawImage4Grid(dc.GetTheme().dropShadow4Grid, x - dropsize * 1.5f, y - dropsize * 1.5f, x + w + dropsize * 1.5f, y + h + dropsize * 1.5f, alphaMul(color, pulse), 1.0f); + + dc.DrawRectDropShadow(iconBounds, 10.0f, pulse, alphaMul(color, pulse)); dc.Draw()->Flush(); } else { dc.Draw()->Flush(); dc.RebindTexture(); - dc.Draw()->DrawImage4Grid(dc.GetTheme().dropShadow4Grid, x - dropsize, y - dropsize * 0.5f, x + w + dropsize, y + h + dropsize * 1.5, alphaMul(shadowColor, 0.5f), 1.0f); + dc.DrawRectDropShadow(iconBounds, 8.0f, 0.7f, shadowColor); + // dc.Draw()->DrawImage4Grid(dc.GetTheme().dropShadow4Grid, x - dropsize, y - dropsize * 0.5f, x + w + dropsize, y + h + dropsize * 1.5, alphaMul(shadowColor, 0.5f), 1.0f); dc.Draw()->Flush(); } dc.Draw()->Flush(); dc.GetDrawContext()->BindTexture(0, texture); - dc.Draw()->DrawTexRect(x, y, x + w, y + h, 0, 0, 1, 1, color); + dc.Draw()->DrawTexRect(iconBounds, 0, 0, 1, 1, color); dc.Draw()->Flush(); } @@ -302,26 +302,11 @@ void GameButton::Draw(UIContext &dc) { idStr = ga->T("SaveData"); } dc.SetFontScale(0.5f * g_Config.fGameGridScale, 0.5f * g_Config.fGameGridScale); - dc.DrawText(idStr, bounds_.x + 5, y + 1, 0xFF000000, ALIGN_TOPLEFT); - dc.DrawText(idStr, bounds_.x + 4, y, dc.GetTheme().infoStyle.fgColor, ALIGN_TOPLEFT); + dc.DrawText(idStr, bounds_.x + 5, iconBounds.y + 1, 0xFF000000, ALIGN_TOPLEFT); + dc.DrawText(idStr, bounds_.x + 4, iconBounds.y, dc.GetTheme().infoStyle.fgColor, ALIGN_TOPLEFT); dc.SetFontScale(1.0f, 1.0f); } - u32 overlayColor = 0; - if (holdEnabled_ && holdStart_ != 0.0) { - double time_held = time_now_d() - holdStart_; - overlayColor = whiteAlpha(time_held / 2.5f); - if (holdStart_ != 0.0) { - double time_held = time_now_d() - holdStart_; - int holdFrameCount = (int)(time_held * 60.0f); - if (holdFrameCount > 60) { - // Blink before launching by holding - if (((holdFrameCount >> 3) & 1) == 0) - overlayColor = 0x0; - } - } - } - if (imageIcon.isValid()) { Style style = dc.GetTheme().itemStyle; @@ -395,11 +380,11 @@ void GameButton::Draw(UIContext &dc) { const AtlasImage *gearImage = dc.Draw()->GetAtlas()->getImage(ImageID("I_GEAR_SMALL")); if (gearImage) { if (viewMode_ == GameBrowserViewMode::GRID) { - dc.Draw()->DrawImage(ImageID("I_GEAR_SMALL"), bounds_.x, y + h - gearImage->h * g_Config.fGameGridScale, g_Config.fGameGridScale); + dc.Draw()->DrawImage(ImageID("I_GEAR_SMALL"), bounds_.x, iconBounds.y2() - gearImage->h * g_Config.fGameGridScale, g_Config.fGameGridScale); } else if (viewMode_ == GameBrowserViewMode::LARGE_LIST) { - dc.Draw()->DrawImage(ImageID("I_GEAR_SMALL"), bounds_.x + LARGE_LIST_LEFT_PAD, y + h - gearImage->h * g_Config.fGameGridScale, g_Config.fGameGridScale); + dc.Draw()->DrawImage(ImageID("I_GEAR_SMALL"), bounds_.x + LARGE_LIST_LEFT_PAD, iconBounds.y2() - gearImage->h * g_Config.fGameGridScale, g_Config.fGameGridScale); } else { - dc.Draw()->DrawImage(ImageID("I_GEAR_SMALL"), bounds_.x + 4, y, 1.0f); + dc.Draw()->DrawImage(ImageID("I_GEAR_SMALL"), bounds_.x + 4, iconBounds.y, 1.0f); } } } @@ -418,12 +403,12 @@ void GameButton::Draw(UIContext &dc) { if (regionImage) { if (viewMode_ == GameBrowserViewMode::GRID) { dc.Draw()->DrawImage(regionIcons[regionIndex], bounds_.x + bounds_.w - (regionImage->w + 5) * g_Config.fGameGridScale, - y + h - (regionImage->h + 5) * g_Config.fGameGridScale, g_Config.fGameGridScale); + iconBounds.y2() - (regionImage->h + 5) * g_Config.fGameGridScale, g_Config.fGameGridScale); } else if (viewMode_ == GameBrowserViewMode::LARGE_LIST) { dc.Draw()->DrawImage(regionIcons[regionIndex], bounds_.x + LARGE_LIST_LEFT_PAD + (LARGE_LIST_ICON_W - regionImage->w - 5) * g_Config.fGameGridScale, - y + h - (regionImage->h + 5) * g_Config.fGameGridScale, g_Config.fGameGridScale); + iconBounds.y2() - (regionImage->h + 5) * g_Config.fGameGridScale, g_Config.fGameGridScale); } else { - dc.Draw()->DrawImage(regionIcons[regionIndex], bounds_.x + 4, y + h - regionImage->h - 5, 1.0f); + dc.Draw()->DrawImage(regionIcons[regionIndex], bounds_.x + 4, iconBounds.y2() - regionImage->h - 5, 1.0f); } } } @@ -440,6 +425,21 @@ void GameButton::Draw(UIContext &dc) { dc.Draw()->DrawImage(ImageID("I_INFO"), infoBounds.centerX(), infoBounds.centerY(), 1.0f, infoStyle.fgColor, ALIGN_CENTER); } + u32 overlayColor = 0; + if (holdEnabled_ && holdStart_ != 0.0) { + double time_held = time_now_d() - holdStart_; + overlayColor = whiteAlpha(time_held / 2.5f); + if (holdStart_ != 0.0) { + double time_held = time_now_d() - holdStart_; + int holdFrameCount = (int)(time_held * 60.0f); + if (holdFrameCount > 60) { + // Blink before launching by holding + if (((holdFrameCount >> 3) & 1) == 0) + overlayColor = 0x0; + } + } + } + if (overlayColor) { dc.FillRect(Drawable(overlayColor), overlayBounds); } @@ -448,7 +448,6 @@ void GameButton::Draw(UIContext &dc) { GamePacker::Status packStatus; if (GamePacker::IsPacking(gamePath_) && GamePacker::GetStatus(&packStatus)) { auto ga = GetI18NCategory(I18NCat::GAME); - const Bounds iconBounds(x, y, w, h); dc.FillRect(Drawable(0xD0505050), iconBounds); const float scale = viewMode_ == GameBrowserViewMode::GRID || viewMode_ == GameBrowserViewMode::LARGE_LIST ? 0.8f * g_Config.fGameGridScale : 0.6f; dc.SetFontStyle(dc.GetTheme().uiFont); From ede471aee6d421a781dc0d30050cd76adfed69f7 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Henrik=20Rydg=C3=A5rd?= Date: Sun, 1 Nov 2026 10:24:34 +0100 Subject: [PATCH 6/8] Merge DirButton into GameButton --- Common/File/Path.h | 2 +- UI/GameBrowser.cpp | 118 ++++++++++-------------------------- UI/GameButton.cpp | 146 ++++++++++++++++++++++++++++++--------------- UI/GameButton.h | 55 +++++++++-------- 4 files changed, 162 insertions(+), 159 deletions(-) diff --git a/Common/File/Path.h b/Common/File/Path.h index ab77b82a3a..534a93e2a0 100644 --- a/Common/File/Path.h +++ b/Common/File/Path.h @@ -59,7 +59,7 @@ public: } bool Valid() const { return !path_.empty(); } - bool IsRoot() const { return path_ == "/"; } // Special value - only path that can end in a slash. + bool IsRoot() const { return path_ == "/"; } // Special value - only path that can end in a slash - except see below. // On Windows, "//" is the network folder that lists the servers, the other path that ends in a slash. bool IsNetworkRoot() const { #if PPSSPP_PLATFORM(WINDOWS) diff --git a/UI/GameBrowser.cpp b/UI/GameBrowser.cpp index 5ee36c6846..99dd05f35e 100644 --- a/UI/GameBrowser.cpp +++ b/UI/GameBrowser.cpp @@ -54,70 +54,6 @@ // Folders get bigger lines too, though not as big as the games. static constexpr float LARGE_LIST_FOLDER_H = 50.0f; -class DirButton : public UI::Button { -public: - DirButton(const Path &path, GameBrowserViewMode viewMode, UI::LayoutParams *layoutParams) - : UI::Button(path.ToString(), layoutParams), path_(path), viewMode_(viewMode), absolute_(false) {} - DirButton(const Path &path, const std::string &text, GameBrowserViewMode viewMode, UI::LayoutParams *layoutParams = 0) - : UI::Button(text, layoutParams), path_(path), viewMode_(viewMode), absolute_(true) {} - - void Draw(UIContext &dc) override; - void GetContentDimensions(const UIContext &dc, float &w, float &h) const override { - UI::Button::GetContentDimensions(dc, w, h); - if (viewMode_ == GameBrowserViewMode::LARGE_LIST) { - h = std::max(h, LARGE_LIST_FOLDER_H * g_Config.fGameGridScale + LARGE_LIST_PAD * 2); - } - } - - const Path &GetPath() const { - return path_; - } - - bool PathAbsolute() const { - return absolute_; - } - - void SetPinned(bool pin) { - pinned_ = pin; - } - - std::string DescribeText() const override { - return std::string(Button::GetText()); // From Button - } - -private: - Path path_; - GameBrowserViewMode viewMode_; - bool absolute_; - bool pinned_ = false; -}; - -void DirButton::Draw(UIContext &dc) { - using namespace UI; - - std::string_view text(GetText()); - ImageID image = ImageID(pinned_ ? "I_FOLDER_PINNED" : "I_FOLDER"); - if (path_.IsNetworkRoot()) { - image = ImageID("I_NETWORK"); - } else if (text == "..") { - image = ImageID("I_UP_DIRECTORY"); - text = ""; - } - - Style style = dc.GetTheme().itemStyle; - - if (HasFocus()) style = dc.GetTheme().itemFocusedStyle; - if (down_) style = dc.GetTheme().itemDownStyle; - if (!IsEnabled()) style = dc.GetTheme().itemDisabledStyle; - - dc.FillRect(style.background, bounds_); - if (viewMode_ == GameBrowserViewMode::GRID) { - DrawIconWithTextGrid(dc, image, text, bounds_, style); - } else { - DrawIconWithTextList(dc, image, text, bounds_, viewMode_, style); - } -} - 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; @@ -138,19 +74,19 @@ GameBrowser::GameBrowser(int token, const Path &path, BrowseFlags browseFlags, b Refresh(); } -void GameBrowser::FocusGame(const Path &gamePath) { +void GameBrowser::FocusGame(const Path &GetPath) { // If the button's still there, just focus it. That keeps the search results and scroll position. if (gameList_) { for (int i = 0; i < gameList_->GetNumSubviews(); i++) { GameButton *button = dynamic_cast(gameList_->GetViewByIndex(i)); - if (button && button->GamePath() == gamePath && button->GetVisibility() == UI::V_VISIBLE) { + if (button && button->GetPath() == GetPath && button->GetVisibility() == UI::V_VISIBLE) { button->SetFocus(UI::FocusFlags::CAUSE_RESTORE); return; } } } // Otherwise, Refresh focuses it when it recreates the buttons. - focusPath_ = gamePath; + focusPath_ = GetPath; refreshPending_ = true; } @@ -490,7 +426,7 @@ void GameBrowser::Refresh() { Add(gameList_); // Find games in the current directory and create new ones. - std::vector dirButtons; + std::vector dirButtons; std::vector gameButtons; listingPending_ = !path_.IsListingReady(); @@ -500,7 +436,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], lastViewMode_, new UI::LinearLayoutParams(gridStyle ? UI::WRAP_CONTENT : UI::FILL_PARENT, UI::WRAP_CONTENT))); + gameButtons.push_back(new GameButton(GameButtonType::Game, filenames[i], lastViewMode_, new UI::LinearLayoutParams(gridStyle ? UI::WRAP_CONTENT : UI::FILL_PARENT, UI::WRAP_CONTENT))); } } else if (!listingPending_) { std::vector fileInfo; @@ -524,11 +460,13 @@ void GameBrowser::Refresh() { if (!isGame && !isSaveData) { if (browseFlags_ & BrowseFlags::NAVIGATE) { - const std::string name = fileInfo[i].fullName.IsNetworkRoot() ? std::string(mm->T("Network")) : fileInfo[i].name; - dirButtons.push_back(new DirButton(fileInfo[i].fullName, name, lastViewMode_, new UI::LinearLayoutParams(UI::FILL_PARENT, UI::FILL_PARENT))); + dirButtons.push_back(new GameButton(GameButtonType::Dir, fileInfo[i].fullName, lastViewMode_, new UI::LinearLayoutParams(UI::FILL_PARENT, UI::FILL_PARENT))); + if (fileInfo[i].fullName.IsNetworkRoot()) { + dirButtons.back()->SetAsAbsolute(); + } } } else { - gameButtons.push_back(new GameButton(fileInfo[i].fullName, lastViewMode_, new UI::LinearLayoutParams(gridStyle ? UI::WRAP_CONTENT : UI::FILL_PARENT, UI::WRAP_CONTENT))); + gameButtons.push_back(new GameButton(GameButtonType::Game, fileInfo[i].fullName, lastViewMode_, new UI::LinearLayoutParams(gridStyle ? UI::WRAP_CONTENT : UI::FILL_PARENT, UI::WRAP_CONTENT))); } } @@ -541,8 +479,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, lastViewMode_, new UI::LinearLayoutParams(UI::FILL_PARENT, UI::WRAP_CONTENT)); - b->SetHoldEnabled(false); + GameButton *b = new GameButton(GameButtonType::Other, fileInfo[i].fullName, lastViewMode_, new UI::LinearLayoutParams(UI::FILL_PARENT, UI::WRAP_CONTENT)); gameButtons.push_back(b); } } @@ -550,19 +487,20 @@ void GameBrowser::Refresh() { } } - DirButton *upButton = nullptr; + GameButton *upButton = nullptr; if (browseFlags_ & BrowseFlags::NAVIGATE) { if (path_.CanNavigateUp()) { - upButton = gameList_->Add(new DirButton(Path(".."), lastViewMode_, new UI::LinearLayoutParams(UI::FILL_PARENT, UI::FILL_PARENT))); + upButton = gameList_->Add(new GameButton(GameButtonType::Dir, Path(".."), lastViewMode_, new UI::LinearLayoutParams(UI::FILL_PARENT, UI::FILL_PARENT))); upButton->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(), lastViewMode_, new UI::LinearLayoutParams(UI::FILL_PARENT, UI::FILL_PARENT))); + GameButton *pinnedDir = gameList_->Add(new GameButton(GameButtonType::Dir, pinnedPath, lastViewMode_, new UI::LinearLayoutParams(UI::FILL_PARENT, UI::FILL_PARENT))); pinnedDir->OnClick.Handle(this, &GameBrowser::NavigateClick); pinnedDir->SetPinned(true); + pinnedDir->SetAsAbsolute(); } } @@ -583,7 +521,7 @@ void GameBrowser::Refresh() { b->OnHoldClick.Handle(this, &GameBrowser::GameButtonHoldClick); b->OnHighlight.Handle(this, &GameBrowser::GameButtonHighlight); - if (!focusPath_.empty() && b->GamePath() == focusPath_ && b->SetFocus(FocusFlags::CAUSE_RESTORE)) { + if (!focusPath_.empty() && b->GetPath() == focusPath_ && b->SetFocus(FocusFlags::CAUSE_RESTORE)) { UI::ScrollFocusedViewIntoView(); } } @@ -601,6 +539,7 @@ void GameBrowser::Refresh() { } // Show a button to toggle pinning at the very end. + // TODO: That's not very friendly. if ((browseFlags_ & BrowseFlags::PIN) && !path_.GetPath().empty()) { std::string caption = ""; // IsCurrentPathPinned() ? "-" : "+"; if (!gridStyle) { @@ -668,16 +607,22 @@ std::vector GameBrowser::GetPinnedPaths() const { } void GameBrowser::GameButtonClick(UI::EventParams &e) { - GameButton *button = static_cast(e.v); + GameButton *button = dynamic_cast(e.v); + if (!button) { + return; + } UI::EventParams e2{}; - e2.s = button->GamePath().ToString(); + e2.s = button->GetPath().ToString(); OnChoice.Trigger(e2); } void GameBrowser::GameButtonHoldClick(UI::EventParams &e) { - GameButton *button = static_cast(e.v); + GameButton *button = dynamic_cast(e.v); + if (!button) { + return; + } UI::EventParams e2{}; - e2.s = button->GamePath().ToString(); + e2.s = button->GetPath().ToString(); OnHoldChoice.Trigger(e2); } @@ -686,15 +631,18 @@ void GameBrowser::GameButtonHighlight(UI::EventParams &e) { } void GameBrowser::NavigateClick(UI::EventParams &e) { - DirButton *button = static_cast(e.v); + GameButton *button = dynamic_cast(e.v); + if (!button) { + return; + } Path text = button->GetPath(); - if (button->PathAbsolute()) { + if (button->NavigateAsAbsolute()) { path_.SetPath(text); } else if (text.ToString() == "..") { NavigateUp(); return; } else { - path_.Navigate(text.ToString()); + path_.Navigate(text.GetFilename()); } focusFirstItem_ = true; PathChanged(); diff --git a/UI/GameButton.cpp b/UI/GameButton.cpp index e93773c06c..60eab60f57 100644 --- a/UI/GameButton.cpp +++ b/UI/GameButton.cpp @@ -96,6 +96,7 @@ static ImageID ChooseGameIcon(const std::shared_ptr &ginfo) { case IdentifiedFileType::ARCHIVE_7Z: return ImageID("I_ARCHIVE_7Z"); break; case IdentifiedFileType::ARCHIVE_RAR: return ImageID("I_ARCHIVE_RAR"); break; case IdentifiedFileType::PSP_PKG: return ImageID("I_FOLDER_UPLOAD"); break; + case IdentifiedFileType::NORMAL_DIRECTORY: return ImageID("I_FOLDER"); break; case IdentifiedFileType::PSP_ISO: case IdentifiedFileType::PSP_ISO_NP: @@ -176,18 +177,88 @@ void DrawIconWithTextList(UIContext &dc, ImageID image, std::string_view text, c } } +void GameButton::GetContentDimensions(const UIContext &dc, float &w, float &h) const { + switch (viewMode_) { + case GameBrowserViewMode::GRID: + w = 144 * g_Config.fGameGridScale; + h = 80 * g_Config.fGameGridScale; + break; + case GameBrowserViewMode::LARGE_LIST: + w = 500; + if (type_ == GameButtonType::Dir) { + h = UI::ITEM_HEIGHT; + } else { + h = LARGE_LIST_ICON_H * g_Config.fGameGridScale + LARGE_LIST_PAD * 2; + } + break; + default: // List + w = 500; + h = 56; + break; + } +} + void GameButton::Draw(UIContext &dc) { // Like the XMB, only the selected game plays its video. Only keyboard/gamepad focus selects, since with // touch or the mouse, getting a video to play without launching the game was too fiddly. const bool wantVideo = viewMode_ == GameBrowserViewMode::LARGE_LIST && HasFocus() && 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); + std::shared_ptr ginfo; + + std::string text; + std::string id_version; Draw::Texture *texture = nullptr; + ImageID imageIcon; + int regionIndex = -1; + IdentifiedFileType fileType = IdentifiedFileType::UNKNOWN; + + if (type_ == GameButtonType::Dir) { + text = gamePath_.GetFilename(); + imageIcon = ImageID(IsPinned() ? "I_FOLDER_PINNED" : "I_FOLDER"); + if (gamePath_.IsNetworkRoot()) { + imageIcon = ImageID("I_NETWORK"); + auto mm = GetI18NCategory(I18NCat::MAINMENU); + text = mm->T("Network"); + } else if (text == "..") { + imageIcon = ImageID("I_UP_DIRECTORY"); + text = ""; + } + fileType = IdentifiedFileType::NORMAL_DIRECTORY; + } else { + GameInfoFlags infoFlags = GameInfoFlags::PARAM_SFO | GameInfoFlags::ICON; + if (wantVideo) { + infoFlags |= GameInfoFlags::ICON1_PMF; + } + ginfo = g_gameInfoCache->GetInfo(dc.GetDrawContext(), gamePath_, infoFlags); + UpdateVideo(ginfo, wantVideo); + imageIcon = ChooseGameIcon(ginfo); + if (ginfo->Ready(GameInfoFlags::ICON)) { + if (ginfo->icon.texture) { + 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. + // Maybe we should show something for other file types too. + imageIcon = ImageID("I_LINK_OUT_QUESTION"); + } + text = ginfo->GetTitle(); + id_version = ginfo->GetTitle(); + regionIndex = (int)ginfo->region; + fileType = ginfo->fileType; + } + + if (video_) { + Draw::Texture *frame = video_->Update(dc.GetDrawContext(), videoStart_.ElapsedSeconds()); + if (frame) { + texture = frame; + } + } + u32 color = 0, shadowColor = 0; using namespace UI; @@ -196,33 +267,6 @@ void GameButton::Draw(UIContext &dc) { style = dc.GetTheme().itemDownStyle; } - // Some types we just draw a default icon for. - ImageID imageIcon = ChooseGameIcon(ginfo); - if (ginfo->Ready(GameInfoFlags::ICON)) { - if (ginfo->icon.texture) { - 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. - // Maybe we should show something for other file types too. - imageIcon = ImageID("I_LINK_OUT_QUESTION"); - } - - Bounds iconBounds = bounds_; - iconBounds.w = viewMode_ == GameBrowserViewMode::GRID ? bounds_.w : 144; - if (viewMode_ == GameBrowserViewMode::LARGE_LIST) { - iconBounds.x += LARGE_LIST_LEFT_PAD; - iconBounds.y += LARGE_LIST_PAD; - iconBounds.w = LARGE_LIST_ICON_W * g_Config.fGameGridScale; - iconBounds.h = LARGE_LIST_ICON_H * g_Config.fGameGridScale; - } - const float textX = ListTextX(viewMode_); - const bool rowFocused = HasFocus() && !infoSelected_; if (viewMode_ != GameBrowserViewMode::GRID || !texture) { if (rowFocused) @@ -236,14 +280,18 @@ void GameButton::Draw(UIContext &dc) { dc.Draw()->Flush(); } - if (texture && video_) { - Draw::Texture *frame = video_->Update(dc.GetDrawContext(), videoStart_.ElapsedSeconds()); - if (frame) { - texture = frame; - } + // Some types we just draw a default icon for. + Bounds iconBounds = bounds_; + iconBounds.w = viewMode_ == GameBrowserViewMode::GRID ? bounds_.w : 144; + if (viewMode_ == GameBrowserViewMode::LARGE_LIST) { + iconBounds.x += LARGE_LIST_LEFT_PAD; + iconBounds.y += LARGE_LIST_PAD; + iconBounds.w = LARGE_LIST_ICON_W * g_Config.fGameGridScale; + iconBounds.h = LARGE_LIST_ICON_H * g_Config.fGameGridScale; } + const float textX = ListTextX(viewMode_); - if (texture) { + if (texture && ginfo) { color = whiteAlpha(ease((time_now_d() - ginfo->icon.timeLoaded) * 2)); shadowColor = blackAlpha(ease((time_now_d() - ginfo->icon.timeLoaded) * 2)); float tw = texture->Width(); @@ -295,9 +343,9 @@ void GameButton::Draw(UIContext &dc) { dc.Draw()->Flush(); } - if (viewMode_ == GameBrowserViewMode::GRID && g_Config.bShowIDOnGameIcon && ginfo->fileType != IdentifiedFileType::PSP_ELF && !ginfo->id_version.empty()) { + if (viewMode_ == GameBrowserViewMode::GRID && g_Config.bShowIDOnGameIcon && fileType != IdentifiedFileType::PSP_ELF && !id_version.empty()) { std::string_view idStr = ginfo->id_version; - if (ginfo->fileType == IdentifiedFileType::PSP_SAVEDATA_DIRECTORY) { + if (fileType == IdentifiedFileType::PSP_SAVEDATA_DIRECTORY) { auto ga = GetI18NCategory(I18NCat::GAME); idStr = ga->T("SaveData"); } @@ -315,17 +363,18 @@ void GameButton::Draw(UIContext &dc) { if (!IsEnabled()) style = dc.GetTheme().itemDisabledStyle; if (viewMode_ == GameBrowserViewMode::GRID) { - DrawIconWithTextGrid(dc, imageIcon, ginfo->GetTitle(), bounds_, style); + DrawIconWithTextGrid(dc, imageIcon, text, bounds_, style); } else { - DrawIconWithTextList(dc, imageIcon, ginfo->GetTitle(), bounds_, viewMode_, style, g_Config.bShowIDOnGameIcon ? std::string_view(ginfo->id_version) : std::string_view()); + DrawIconWithTextList(dc, imageIcon, text, bounds_, viewMode_, style, g_Config.bShowIDOnGameIcon ? std::string_view(id_version) : std::string_view()); } } else { + /* char discNumInfo[8]; if (ginfo->disc_total > 1) snprintf(discNumInfo, sizeof(discNumInfo), "-DISC%d", ginfo->disc_number); else discNumInfo[0] = '\0'; - + */ dc.Draw()->Flush(); dc.RebindTexture(); dc.SetFontStyle(dc.GetTheme().uiFont); @@ -333,7 +382,7 @@ void GameButton::Draw(UIContext &dc) { float tw, th; dc.Draw()->Flush(); dc.PushScissor(bounds_); - const std::string currentTitle = ginfo->GetTitle(); + const std::string currentTitle = text; if (!currentTitle.empty()) { title_ = ReplaceAll(currentTitle, "\n", " "); } @@ -343,10 +392,10 @@ void GameButton::Draw(UIContext &dc) { int availableWidth = bounds_.w - textX - ListTextRightMargin(viewMode_); if (g_Config.bShowIDOnGameIcon) { float vw, vh; - dc.MeasureText(dc.GetFontStyle(), 0.7f, 0.7f, ginfo->id_version, &vw, &vh, 0); + dc.MeasureText(dc.GetFontStyle(), 0.7f, 0.7f, id_version, &vw, &vh, 0); availableWidth -= vw + 20; dc.SetFontScale(0.7f, 0.7f); - dc.DrawText(ginfo->id_version, bounds_.x + availableWidth + textX + 10, bounds_.centerY(), style.fgColor, ALIGN_VCENTER); + dc.DrawText(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; @@ -376,7 +425,7 @@ void GameButton::Draw(UIContext &dc) { } } - if (ginfo->hasConfig && !ginfo->id.empty()) { + if (ginfo && ginfo->hasConfig && !ginfo->id.empty()) { const AtlasImage *gearImage = dc.Draw()->GetAtlas()->getImage(ImageID("I_GEAR_SMALL")); if (gearImage) { if (viewMode_ == GameBrowserViewMode::GRID) { @@ -389,7 +438,6 @@ void GameButton::Draw(UIContext &dc) { } } - const int regionIndex = (int)ginfo->region; if (g_Config.bShowRegionOnGameIcon && regionIndex >= 0 && regionIndex < (int)GameRegion::COUNT) { const ImageID regionIcons[(int)GameRegion::COUNT] = { ImageID("I_FLAG_JP"), diff --git a/UI/GameButton.h b/UI/GameButton.h index 5d222b1db8..3a691f91aa 100644 --- a/UI/GameButton.h +++ b/UI/GameButton.h @@ -40,10 +40,16 @@ static constexpr float LARGE_LIST_PAD = 6.0f; static constexpr float LARGE_LIST_LEFT_PAD = 11.0f; static constexpr float LARGE_LIST_INFO_SIZE = 64.0f; +enum class GameButtonType { + Game, + Other, + Dir, +}; + class GameButton : public UI::Clickable { public: - GameButton(const Path &gamePath, GameBrowserViewMode viewMode, UI::LayoutParams *layoutParams = nullptr) - : UI::Clickable(layoutParams), viewMode_(viewMode), gamePath_(gamePath) {} + GameButton(GameButtonType type, const Path &gamePath, GameBrowserViewMode viewMode, UI::LayoutParams *layoutParams = nullptr) + : UI::Clickable(layoutParams), type_(type), viewMode_(viewMode), gamePath_(gamePath) {} void Draw(UIContext &dc) override; std::string DescribeText() const override; @@ -57,26 +63,10 @@ public: } return filename; } - void GetContentDimensions(const UIContext &dc, float &w, float &h) const override { - switch (viewMode_) { - case GameBrowserViewMode::GRID: - w = 144 * g_Config.fGameGridScale; - h = 80 * g_Config.fGameGridScale; - break; - case GameBrowserViewMode::LARGE_LIST: - w = 500; - h = LARGE_LIST_ICON_H * g_Config.fGameGridScale + LARGE_LIST_PAD * 2; - break; - default: // List - w = 500; - h = 50; - break; - } - } - const Path &GamePath() const { return gamePath_; } + void GetContentDimensions(const UIContext &dc, float &w, float &h) const override; - void SetHoldEnabled(bool hold) { - holdEnabled_ = hold; + bool IsHoldEnabled() const { + return type_ == GameButtonType::Game; } bool Touch(const TouchInput &input) override { hovering_ = bounds_.Contains(input.x, input.y); @@ -133,7 +123,7 @@ public: } // Holding the accept button works like a long press, so a gamepad can do it too. - if (holdEnabled_ && HasFocus() && UI::IsAcceptKey(key) && !(key.flags & KeyInputFlags::IS_REPEAT)) { + if (IsHoldEnabled() && HasFocus() && UI::IsAcceptKey(key) && !(key.flags & KeyInputFlags::IS_REPEAT)) { if (key.flags & KeyInputFlags::DOWN) { holdStart_ = time_now_d(); } @@ -147,7 +137,7 @@ public: void Update() override { // Hold button for 1.5 seconds to launch the game options - if (holdEnabled_ && holdStart_ != 0.0 && holdStart_ < time_now_d() - 1.5) { + if (IsHoldEnabled() && holdStart_ != 0.0 && holdStart_ < time_now_d() - 1.5) { TriggerOnHoldClick(); } } @@ -189,6 +179,17 @@ public: } } + // Previously in DirButton + + // This only determines if the little pin icon is shown. + void SetPinned(bool pin) { pinned_ = pin; } + bool IsPinned() const { return pinned_; } + + void SetAsAbsolute() { absolute_ = true; } + bool NavigateAsAbsolute() const { return absolute_; } + + const Path &GetPath() const { return gamePath_; } + UI::Event OnHoldClick; UI::Event OnHighlight; @@ -213,7 +214,7 @@ private: // The large list has room for an info button on each line, doing the same as a long press. bool HasInfoButton() const { - return viewMode_ == GameBrowserViewMode::LARGE_LIST && holdEnabled_; + return viewMode_ == GameBrowserViewMode::LARGE_LIST && type_ == GameButtonType::Game; } Bounds InfoButtonBounds() const { return Bounds(bounds_.x2() - LARGE_LIST_INFO_SIZE - LARGE_LIST_PAD, bounds_.centerY() - LARGE_LIST_INFO_SIZE * 0.5f, LARGE_LIST_INFO_SIZE, LARGE_LIST_INFO_SIZE); @@ -255,6 +256,12 @@ private: bool infoButtonDown_ = false; // The info button has the keyboard focus, rather than the game. Changed from CanMoveFocus, hence mutable. mutable bool infoSelected_ = false; + + // Previously in DirButton + bool pinned_ = false; + bool absolute_ = false; + + GameButtonType type_; }; // Also used for the folder buttons. From b11a2b173e172991c333b3f14c6aa4daf5036d66 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Henrik=20Rydg=C3=A5rd?= Date: Sun, 1 Nov 2026 10:29:43 +0100 Subject: [PATCH 7/8] inline the image drawing functions --- UI/GameButton.cpp | 139 ++++++++++++++++++++++------------------------ 1 file changed, 67 insertions(+), 72 deletions(-) diff --git a/UI/GameButton.cpp b/UI/GameButton.cpp index 60eab60f57..f664ca66a3 100644 --- a/UI/GameButton.cpp +++ b/UI/GameButton.cpp @@ -107,76 +107,6 @@ static ImageID ChooseGameIcon(const std::shared_ptr &ginfo) { } } -// Grid style. Fallback -void DrawIconWithTextGrid(UIContext &dc, ImageID image, std::string_view text, const Bounds &bounds, const UI::Style &style) { - float tw, th; - dc.MeasureText(dc.GetFontStyle(), g_Config.fGameGridScale, g_Config.fGameGridScale, text, &tw, &th, 0); - dc.PushScissor(bounds); - const FontStyle *fontStyle = GetTextStyle(dc, UI::TextSize::Small); - dc.SetFontStyle(*GetTextStyle(dc, UI::TextSize::Small)); - - int iconSize = image == ImageID("I_UP_DIRECTORY") ? (float)dc.Draw()->GetAtlas()->getImage(image)->h : bounds.h * 0.3f; - float textWidth = 0.0f; - float textHeight = 0; - dc.MeasureTextRect(*fontStyle, 1.0f, 1.0f, text, bounds.w - 10, &textWidth, &textHeight, ALIGN_HCENTER | FLAG_WRAP_TEXT); - - int totalHeight = iconSize + (int)textHeight; - - const float y = std::max(0.0f, bounds.h / 2.0f - totalHeight / 2.0f); - - if (image.isValid()) { - const AtlasImage *img = dc.Draw()->GetAtlas()->getImage(image); - if (img && img->h > 0) { - dc.RebindTexture(); - dc.Draw()->DrawImage(image, bounds.centerX(), bounds.y + y + 2, iconSize / (float)img->h, style.fgColor, ALIGN_TOP | ALIGN_HCENTER); - } - } - - if (image != ImageID("I_UP_DIRECTORY") && image != ImageID("I_PIN") && image != ImageID("I_UNPIN")) { - dc.DrawTextRect(text, bounds.Inset(5, y + iconSize + 4, 5, 2), style.fgColor, ALIGN_HCENTER | FLAG_WRAP_TEXT); - } - dc.SetFontStyle(dc.GetTheme().uiFont); - dc.PopScissor(); -} - -// List, large list -// In the list views, rightText (like a game ID) goes at the right edge, as for a game with a picture. -void DrawIconWithTextList(UIContext &dc, ImageID image, std::string_view text, const Bounds &bounds, GameBrowserViewMode viewMode, const UI::Style &style, std::string_view rightText) { - float tw, th; - dc.MeasureText(dc.GetFontStyle(), 1.0f, 1.0f, text, &tw, &th, 0); - - const float textX = ListTextX(viewMode); - if (viewMode == GameBrowserViewMode::LARGE_LIST) { - const float scale = g_Config.fGameGridScale; - dc.Draw()->DrawImage(image, bounds.x + LARGE_LIST_LEFT_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, style.fgColor, ALIGN_CENTER); - } - - float tx = textX; - int availableWidth = bounds.w - textX - ListTextRightMargin(viewMode); - if (!rightText.empty()) { - float vw, vh; - dc.MeasureText(dc.GetFontStyle(), 0.7f, 0.7f, rightText, &vw, &vh, 0); - availableWidth -= vw + 20; - dc.SetFontScale(0.7f, 0.7f); - dc.DrawText(rightText, 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; - if (availableWidth < tw) { - tx -= (1.0f + sin(time_now_d() * 1.5f)) * sineWidth; - Bounds tb = bounds; - tb.x = bounds.x + textX; - tb.w = availableWidth; - dc.PushScissor(tb); - } - dc.DrawText(text, bounds.x + tx, bounds.centerY(), style.fgColor, ALIGN_VCENTER | FLAG_WRAP_TEXT); - if (availableWidth < tw) { - dc.PopScissor(); - } -} - void GameButton::GetContentDimensions(const UIContext &dc, float &w, float &h) const { switch (viewMode_) { case GameBrowserViewMode::GRID: @@ -363,9 +293,74 @@ void GameButton::Draw(UIContext &dc) { if (!IsEnabled()) style = dc.GetTheme().itemDisabledStyle; if (viewMode_ == GameBrowserViewMode::GRID) { - DrawIconWithTextGrid(dc, imageIcon, text, bounds_, style); + Bounds bounds = bounds_; + + float tw, th; + dc.MeasureText(dc.GetFontStyle(), g_Config.fGameGridScale, g_Config.fGameGridScale, text, &tw, &th, 0); + dc.PushScissor(bounds); + const FontStyle *fontStyle = GetTextStyle(dc, UI::TextSize::Small); + dc.SetFontStyle(*GetTextStyle(dc, UI::TextSize::Small)); + + int iconSize = imageIcon == ImageID("I_UP_DIRECTORY") ? (float)dc.Draw()->GetAtlas()->getImage(imageIcon)->h : bounds.h * 0.3f; + float textWidth = 0.0f; + float textHeight = 0; + dc.MeasureTextRect(*fontStyle, 1.0f, 1.0f, text, bounds.w - 10, &textWidth, &textHeight, ALIGN_HCENTER | FLAG_WRAP_TEXT); + + int totalHeight = iconSize + (int)textHeight; + + const float y = std::max(0.0f, bounds.h / 2.0f - totalHeight / 2.0f); + + if (imageIcon.isValid()) { + const AtlasImage *img = dc.Draw()->GetAtlas()->getImage(imageIcon); + if (img && img->h > 0) { + dc.RebindTexture(); + dc.Draw()->DrawImage(imageIcon, bounds.centerX(), bounds.y + y + 2, iconSize / (float)img->h, style.fgColor, ALIGN_TOP | ALIGN_HCENTER); + } + } + + if (imageIcon != ImageID("I_UP_DIRECTORY") && imageIcon != ImageID("I_PIN") && imageIcon != ImageID("I_UNPIN")) { + dc.DrawTextRect(text, bounds.Inset(5, y + iconSize + 4, 5, 2), style.fgColor, ALIGN_HCENTER | FLAG_WRAP_TEXT); + } + dc.SetFontStyle(dc.GetTheme().uiFont); + dc.PopScissor(); } else { - DrawIconWithTextList(dc, imageIcon, text, bounds_, viewMode_, style, g_Config.bShowIDOnGameIcon ? std::string_view(id_version) : std::string_view()); + Bounds bounds = bounds_; + + std::string_view rightText = g_Config.bShowIDOnGameIcon ? std::string_view(id_version) : std::string_view(); + + float tw, th; + dc.MeasureText(dc.GetFontStyle(), 1.0f, 1.0f, text, &tw, &th, 0); + + 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); + } else { + dc.Draw()->DrawImage(imageIcon, bounds.x + 72, bounds.centerY(), 0.88f, style.fgColor, ALIGN_CENTER); + } + + float tx = textX; + int availableWidth = bounds.w - textX - ListTextRightMargin(viewMode_); + if (!rightText.empty()) { + float vw, vh; + dc.MeasureText(dc.GetFontStyle(), 0.7f, 0.7f, rightText, &vw, &vh, 0); + availableWidth -= vw + 20; + dc.SetFontScale(0.7f, 0.7f); + dc.DrawText(rightText, 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; + if (availableWidth < tw) { + tx -= (1.0f + sin(time_now_d() * 1.5f)) * sineWidth; + Bounds tb = bounds; + tb.x = bounds.x + textX; + tb.w = availableWidth; + dc.PushScissor(tb); + } + dc.DrawText(text, bounds.x + tx, bounds.centerY(), style.fgColor, ALIGN_VCENTER | FLAG_WRAP_TEXT); + if (availableWidth < tw) { + dc.PopScissor(); + } } } else { /* From ec1246b2219a4847cfc326c490cde4ef3abc910d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Henrik=20Rydg=C3=A5rd?= Date: Sun, 1 Nov 2026 10:49:59 +0100 Subject: [PATCH 8/8] Deduplicate text rendering code from GameButton --- UI/GameButton.cpp | 185 +++++++++++++++++----------------------------- 1 file changed, 67 insertions(+), 118 deletions(-) diff --git a/UI/GameButton.cpp b/UI/GameButton.cpp index f664ca66a3..452bcd2e5f 100644 --- a/UI/GameButton.cpp +++ b/UI/GameButton.cpp @@ -128,6 +128,15 @@ void GameButton::GetContentDimensions(const UIContext &dc, float &w, float &h) c } } +static const ImageID regionIcons[(int)GameRegion::COUNT] = { + ImageID("I_FLAG_JP"), + ImageID("I_FLAG_US"), + ImageID("I_FLAG_EU"), + ImageID("I_FLAG_HK"), + ImageID("I_FLAG_AS"), + ImageID("I_FLAG_KO"), +}; + void GameButton::Draw(UIContext &dc) { // Like the XMB, only the selected game plays its video. Only keyboard/gamepad focus selects, since with // touch or the mouse, getting a video to play without launching the game was too fiddly. @@ -139,7 +148,7 @@ void GameButton::Draw(UIContext &dc) { std::string id_version; Draw::Texture *texture = nullptr; ImageID imageIcon; - int regionIndex = -1; + GameRegion region = GameRegion::UNKNOWN; IdentifiedFileType fileType = IdentifiedFileType::UNKNOWN; if (type_ == GameButtonType::Dir) { @@ -177,9 +186,15 @@ void GameButton::Draw(UIContext &dc) { imageIcon = ImageID("I_LINK_OUT_QUESTION"); } text = ginfo->GetTitle(); - id_version = ginfo->GetTitle(); - regionIndex = (int)ginfo->region; + text = ReplaceAll(text, "\n", " "); + + id_version = ginfo->id_version; + region = ginfo->region; fileType = ginfo->fileType; + + if (ginfo->disc_total > 1) { + text += StringFromFormat("-DISC%d", ginfo->disc_number); + } } if (video_) { @@ -193,17 +208,13 @@ void GameButton::Draw(UIContext &dc) { using namespace UI; UI::Style style = dc.GetTheme().itemStyle; - if (down_) { - style = dc.GetTheme().itemDownStyle; - } - const bool rowFocused = HasFocus() && !infoSelected_; + if (rowFocused) style = dc.GetTheme().itemFocusedStyle; + if (down_) style = dc.GetTheme().itemDownStyle; + if (!IsEnabled()) style = dc.GetTheme().itemDisabledStyle; + if (viewMode_ != GameBrowserViewMode::GRID || !texture) { - if (rowFocused) - style = down_ ? dc.GetTheme().itemDownStyle : dc.GetTheme().itemFocusedStyle; - Drawable bg = style.background; - dc.Draw()->Flush(); dc.RebindTexture(); dc.FillRect(bg, bounds_); @@ -241,7 +252,7 @@ void GameButton::Draw(UIContext &dc) { Bounds overlayBounds = bounds_; - // Render button + // Render the icon, all modes have this. int dropsize = 10; if (texture) { if (viewMode_ == GameBrowserViewMode::LIST) { @@ -274,7 +285,7 @@ void GameButton::Draw(UIContext &dc) { } if (viewMode_ == GameBrowserViewMode::GRID && g_Config.bShowIDOnGameIcon && fileType != IdentifiedFileType::PSP_ELF && !id_version.empty()) { - std::string_view idStr = ginfo->id_version; + std::string_view idStr = id_version; if (fileType == IdentifiedFileType::PSP_SAVEDATA_DIRECTORY) { auto ga = GetI18NCategory(I18NCat::GAME); idStr = ga->T("SaveData"); @@ -285,14 +296,8 @@ void GameButton::Draw(UIContext &dc) { dc.SetFontScale(1.0f, 1.0f); } - if (imageIcon.isValid()) { - Style style = dc.GetTheme().itemStyle; - - if (rowFocused) style = dc.GetTheme().itemFocusedStyle; - if (down_) style = dc.GetTheme().itemDownStyle; - if (!IsEnabled()) style = dc.GetTheme().itemDisabledStyle; - - if (viewMode_ == GameBrowserViewMode::GRID) { + if (viewMode_ == GameBrowserViewMode::GRID) { + if (imageIcon.isValid()) { Bounds bounds = bounds_; float tw, th; @@ -310,27 +315,34 @@ void GameButton::Draw(UIContext &dc) { const float y = std::max(0.0f, bounds.h / 2.0f - totalHeight / 2.0f); - if (imageIcon.isValid()) { - const AtlasImage *img = dc.Draw()->GetAtlas()->getImage(imageIcon); - if (img && img->h > 0) { - dc.RebindTexture(); - dc.Draw()->DrawImage(imageIcon, bounds.centerX(), bounds.y + y + 2, iconSize / (float)img->h, style.fgColor, ALIGN_TOP | ALIGN_HCENTER); - } + const AtlasImage *img = dc.Draw()->GetAtlas()->getImage(imageIcon); + if (img && img->h > 0) { + dc.RebindTexture(); + dc.Draw()->DrawImage(imageIcon, bounds.centerX(), bounds.y + y + 2, iconSize / (float)img->h, style.fgColor, ALIGN_TOP | ALIGN_HCENTER); } if (imageIcon != ImageID("I_UP_DIRECTORY") && imageIcon != ImageID("I_PIN") && imageIcon != ImageID("I_UNPIN")) { dc.DrawTextRect(text, bounds.Inset(5, y + iconSize + 4, 5, 2), style.fgColor, ALIGN_HCENTER | FLAG_WRAP_TEXT); } - dc.SetFontStyle(dc.GetTheme().uiFont); dc.PopScissor(); - } else { - Bounds bounds = bounds_; + dc.SetFontStyle(dc.GetTheme().uiFont); + } else if (!texture) { + // Just draw the text, centered. + dc.Draw()->Flush(); + dc.PushScissor(bounds_); + dc.DrawText(title_, bounds_.x + 4, bounds_.centerY(), style.fgColor, ALIGN_VCENTER); + dc.Draw()->Flush(); + dc.PopScissor(); + } + } else { + Bounds bounds = bounds_; - std::string_view rightText = g_Config.bShowIDOnGameIcon ? std::string_view(id_version) : std::string_view(); + std::string_view rightText = g_Config.bShowIDOnGameIcon ? std::string_view(id_version) : std::string_view(); - float tw, th; - dc.MeasureText(dc.GetFontStyle(), 1.0f, 1.0f, text, &tw, &th, 0); + float tw, th; + dc.MeasureText(dc.GetFontStyle(), 1.0f, 1.0f, text, &tw, &th, 0); + if (imageIcon.isValid()) { const float textX = ListTextX(viewMode_); if (viewMode_ == GameBrowserViewMode::LARGE_LIST) { const float scale = g_Config.fGameGridScale; @@ -338,85 +350,29 @@ void GameButton::Draw(UIContext &dc) { } else { dc.Draw()->DrawImage(imageIcon, bounds.x + 72, bounds.centerY(), 0.88f, style.fgColor, ALIGN_CENTER); } - - float tx = textX; - int availableWidth = bounds.w - textX - ListTextRightMargin(viewMode_); - if (!rightText.empty()) { - float vw, vh; - dc.MeasureText(dc.GetFontStyle(), 0.7f, 0.7f, rightText, &vw, &vh, 0); - availableWidth -= vw + 20; - dc.SetFontScale(0.7f, 0.7f); - dc.DrawText(rightText, 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; - if (availableWidth < tw) { - tx -= (1.0f + sin(time_now_d() * 1.5f)) * sineWidth; - Bounds tb = bounds; - tb.x = bounds.x + textX; - tb.w = availableWidth; - dc.PushScissor(tb); - } - dc.DrawText(text, bounds.x + tx, bounds.centerY(), style.fgColor, ALIGN_VCENTER | FLAG_WRAP_TEXT); - if (availableWidth < tw) { - dc.PopScissor(); - } } - } else { - /* - char discNumInfo[8]; - if (ginfo->disc_total > 1) - snprintf(discNumInfo, sizeof(discNumInfo), "-DISC%d", ginfo->disc_number); - else - discNumInfo[0] = '\0'; - */ - dc.Draw()->Flush(); - dc.RebindTexture(); - dc.SetFontStyle(dc.GetTheme().uiFont); - if (viewMode_ != GameBrowserViewMode::GRID) { - float tw, th; - dc.Draw()->Flush(); - dc.PushScissor(bounds_); - const std::string currentTitle = text; - if (!currentTitle.empty()) { - title_ = ReplaceAll(currentTitle, "\n", " "); - } - dc.MeasureText(dc.GetFontStyle(), 1.0f, 1.0f, title_, &tw, &th, 0); - - int availableWidth = bounds_.w - textX - ListTextRightMargin(viewMode_); - if (g_Config.bShowIDOnGameIcon) { - float vw, vh; - dc.MeasureText(dc.GetFontStyle(), 0.7f, 0.7f, id_version, &vw, &vh, 0); - availableWidth -= vw + 20; - dc.SetFontScale(0.7f, 0.7f); - dc.DrawText(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 = textX; - if (availableWidth < tw) { - tx -= (1.0f + sin(time_now_d() * 1.5f)) * sineWidth; - Bounds tb = bounds_; - tb.x = bounds_.x + textX; - tb.w = availableWidth; - dc.PushScissor(tb); - } - dc.DrawText(title_, bounds_.x + tx, bounds_.centerY(), style.fgColor, ALIGN_VCENTER); - if (availableWidth < tw) { - dc.PopScissor(); - } - dc.Draw()->Flush(); + float tx = textX; + int availableWidth = bounds.w - textX - ListTextRightMargin(viewMode_); + if (!rightText.empty()) { + float vw, vh; + dc.MeasureText(dc.GetFontStyle(), 0.7f, 0.7f, rightText, &vw, &vh, 0); + availableWidth -= vw + 20; + dc.SetFontScale(0.7f, 0.7f); + dc.DrawText(rightText, 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; + if (availableWidth < tw) { + tx -= (1.0f + sin(time_now_d() * 1.5f)) * sineWidth; + Bounds tb = bounds; + tb.x = bounds.x + textX; + tb.w = availableWidth; + dc.PushScissor(tb); + } + dc.DrawText(text, bounds.x + tx, bounds.centerY(), style.fgColor, ALIGN_VCENTER | FLAG_WRAP_TEXT); + if (availableWidth < tw) { dc.PopScissor(); - } else if (!texture) { - dc.Draw()->Flush(); - dc.PushScissor(bounds_); - dc.DrawText(title_, bounds_.x + 4, bounds_.centerY(), style.fgColor, ALIGN_VCENTER); - dc.Draw()->Flush(); - dc.PopScissor(); - } else { - dc.Draw()->Flush(); } } @@ -433,15 +389,8 @@ void GameButton::Draw(UIContext &dc) { } } - if (g_Config.bShowRegionOnGameIcon && regionIndex >= 0 && regionIndex < (int)GameRegion::COUNT) { - const ImageID regionIcons[(int)GameRegion::COUNT] = { - ImageID("I_FLAG_JP"), - ImageID("I_FLAG_US"), - ImageID("I_FLAG_EU"), - ImageID("I_FLAG_HK"), - ImageID("I_FLAG_AS"), - ImageID("I_FLAG_KO"), - }; + const int regionIndex = (int)region; + if (g_Config.bShowRegionOnGameIcon && region != GameRegion::UNKNOWN && regionIndex >= 0 && regionIndex < (int)GameRegion::COUNT) { const AtlasImage *regionImage = dc.Draw()->GetAtlas()->getImage(regionIcons[regionIndex]); if (regionImage) { if (viewMode_ == GameBrowserViewMode::GRID) {