From f11e15c167aec0684f2a73aac7b935971fc095f4 Mon Sep 17 00:00:00 2001 From: DefKorns Date: Wed, 21 Oct 2026 08:40:39 +0000 Subject: [PATCH 1/7] Move the list-screen chrome, row and note helpers into the framework --- Makefile | 2 +- src/framework/list_screen.cpp | 118 ++++++++++++++++++++++++++++++++++ src/framework/list_screen.h | 61 ++++++++++++++++++ src/framework/text_layout.cpp | 11 ++++ src/framework/text_layout.h | 2 + src/mod_uninstall.cpp | 64 +++--------------- src/save_manager.cpp | 98 ++++------------------------ 7 files changed, 214 insertions(+), 142 deletions(-) create mode 100644 src/framework/list_screen.cpp create mode 100644 src/framework/list_screen.h diff --git a/Makefile b/Makefile index 34b686c..bf8edcb 100644 --- a/Makefile +++ b/Makefile @@ -36,7 +36,7 @@ VENDORED_LIBS = endif CXXFLAGS = -std=c++14 -Os $(SDL_CFLAGS) -DMOD_VERSION=\"v$(MOD_VER)\" LDLIBS = $(SDL_LIBS) -SOURCES = src/main.cpp src/back_stack.cpp src/command.cpp src/download_screen.cpp src/localization.cpp src/framework/badge.cpp src/framework/dialog.cpp src/framework/sdl_context.cpp src/framework/texture.cpp src/framework/controller.cpp src/framework/powerwatch.cpp src/framework/draw_helpers.cpp src/framework/utf8.cpp src/framework/font8x8_lookup.cpp src/framework/uitheme.cpp src/framework/list_jump.cpp src/framework/text_layout.cpp +SOURCES = src/main.cpp src/back_stack.cpp src/command.cpp src/download_screen.cpp src/localization.cpp src/framework/badge.cpp src/framework/dialog.cpp src/framework/sdl_context.cpp src/framework/texture.cpp src/framework/controller.cpp src/framework/powerwatch.cpp src/framework/draw_helpers.cpp src/framework/utf8.cpp src/framework/font8x8_lookup.cpp src/framework/uitheme.cpp src/framework/list_jump.cpp src/framework/text_layout.cpp src/framework/list_screen.cpp OBJECTS = $(SOURCES:.cpp=.o) ALL_OBJECTS = $(OBJECTS) src/daemon.o src/mod_uninstall.o src/save_manager.o src/standby_watchdog.o src/gen_splash.o src/change_combo.o diff --git a/src/framework/list_screen.cpp b/src/framework/list_screen.cpp new file mode 100644 index 0000000..46eed27 --- /dev/null +++ b/src/framework/list_screen.cpp @@ -0,0 +1,118 @@ +/** + * Copyright (c) 2026 DefKorns (https://defkorns.github.io/LICENSE) + * + * 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, either version 3 of the License, or + * (at your option) any later version. + * + * 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 for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +#include "list_screen.h" +#include "draw_helpers.h" +#include "text_layout.h" +#include "uitheme.h" + +namespace +{ + constexpr int CreditFontSize = 16; + constexpr int SectionAccentGap = 14; + constexpr int FooterDividerInset = 10; + constexpr int FooterDividerW = 2; + constexpr int SelectedRowStrokeW = 2; + constexpr int RowSeparatorGap = 3; + + constexpr int NoteFontSize = 15; + constexpr int NoteLinePitch = 20; + constexpr int NoteIconOffsetX = 26; + constexpr int NoteIconSize = 18; + constexpr int NoteTextOffsetX = 46; + constexpr int NotePadRight = 16; +} + +ScreenChrome::ScreenChrome(const std::string & optionsLocation, SDL_Renderer * renderer, const std::string & appTitle, + const std::string & version, const std::string & credit) + : renderer_(renderer) + , gear_(optionsLocation + UiTheme::AssetGear, renderer, UiTheme::GearX, UiTheme::GearY) + , appTitle_(appTitle, UiTheme::TitleFontSize, renderer, UiTheme::TitleX, UiTheme::TitleY, false, ToAbgr(UiTheme::Text), true) + , credit_(credit, CreditFontSize, renderer, UiTheme::CreditX, UiTheme::CreditY, false, ToAbgr(UiTheme::Text), true) +{ + appTitle_.rect.y -= appTitle_.rect.h / 2; + appVersion_ = Texture(version, UiTheme::VersionFontSize, renderer, appTitle_.rect.x + appTitle_.rect.w + UiTheme::VersionGap, UiTheme::TitleY, false, ToAbgr(UiTheme::Text), true); + appVersion_.rect.y -= appVersion_.rect.h / 2; +} + +void ScreenChrome::SetSectionTitle(const std::string & title) +{ + sectionTitle_ = Texture(title, UiTheme::SectionTitleFontSize, renderer_, UiTheme::SectionTitleX, UiTheme::SectionTitleY, false, ToAbgr(UiTheme::Text), true); +} + +void ScreenChrome::Draw() +{ + DrawStrokeRect(renderer_, UiTheme::OuterRect, UiTheme::Border, UiTheme::BorderWidth, UiTheme::BorderRadius); + gear_.Draw(renderer_); + appTitle_.Draw(renderer_); + appVersion_.Draw(renderer_); + DrawHLine(renderer_, UiTheme::HeaderDividerX, UiTheme::HeaderDividerX + UiTheme::HeaderDividerW, UiTheme::HeaderDividerY, UiTheme::Border, UiTheme::BorderWidth); + DrawHLine(renderer_, UiTheme::OuterRect.x, UiTheme::OuterRect.x + UiTheme::OuterRect.w, UiTheme::FooterDividerY, UiTheme::Border, UiTheme::BorderWidth); + const int accentBarY = sectionTitle_.rect.y + (sectionTitle_.rect.h - UiTheme::SectionAccentBarH) / 2; + DrawFillRect(renderer_, { UiTheme::SectionTitleX - UiTheme::SectionAccentBarW - SectionAccentGap, accentBarY, UiTheme::SectionAccentBarW, UiTheme::SectionAccentBarH }, UiTheme::Accent); + sectionTitle_.Draw(renderer_); + credit_.Draw(renderer_); +} + +void DrawFooterDivider(SDL_Renderer * renderer, int rightEdge) +{ + const int dividerX = rightEdge - UiTheme::BadgeDividerGapFromCluster; + DrawVLine(renderer, dividerX, UiTheme::FooterY + FooterDividerInset, UiTheme::FooterY + UiTheme::FooterH - FooterDividerInset, UiTheme::Border, FooterDividerW); +} + +SDL_Rect DrawListRow(SDL_Renderer * renderer, int index, int rightX, int rowHeight, bool selected) +{ + const SDL_Rect rowRect{ UiTheme::ListX, UiTheme::RowFirstY + index * UiTheme::RowPitch, rightX - UiTheme::ListX, rowHeight }; + if(selected) + { + DrawRoundedFillRect(renderer, rowRect, UiTheme::SelectedRowBg, UiTheme::BoxRadius); + DrawStrokeRect(renderer, rowRect, UiTheme::Accent, SelectedRowStrokeW, UiTheme::BoxRadius); + } + else + DrawHLine(renderer, rowRect.x, rowRect.x + rowRect.w, rowRect.y + rowRect.h + RowSeparatorGap, UiTheme::Border, 1); + return rowRect; +} + +NoteBox::NoteBox(SDL_Renderer * renderer, SDL_Rect rect) + : renderer_(renderer) + , rect_(rect) +{ +} + +void NoteBox::SetText(const std::string & text) +{ + const int textW = rect_.w - NotePadRight - NoteTextOffsetX; + lines_ = MakeLines(WrapToWidth(text, NoteFontSize, textW), NoteFontSize, renderer_, UiTheme::TextDim); +} + +void NoteBox::Draw() +{ + DrawStrokeRect(renderer_, rect_, UiTheme::Border, UiTheme::BorderWidth, UiTheme::BoxRadius); + const int iconCX = rect_.x + NoteIconOffsetX; + const int centerY = rect_.y + rect_.h / 2; + DrawStrokeRect(renderer_, { iconCX - NoteIconSize / 2, centerY - NoteIconSize / 2, NoteIconSize, NoteIconSize }, UiTheme::TextDim, 2, NoteIconSize / 2); + DrawFillRect(renderer_, { iconCX - 1, centerY - 5, 2, 2 }, UiTheme::TextDim); + DrawFillRect(renderer_, { iconCX - 1, centerY - 1, 2, 6 }, UiTheme::TextDim); + int y = centerY - static_cast(lines_.size()) * NoteLinePitch / 2; + for(Texture & line : lines_) + { + line.rect.x = rect_.x + NoteTextOffsetX; + line.rect.y = y + (NoteLinePitch - line.rect.h) / 2; + line.Draw(renderer_); + y += NoteLinePitch; + } +} diff --git a/src/framework/list_screen.h b/src/framework/list_screen.h new file mode 100644 index 0000000..ef68e12 --- /dev/null +++ b/src/framework/list_screen.h @@ -0,0 +1,61 @@ +/** + * Copyright (c) 2026 DefKorns (https://defkorns.github.io/LICENSE) + * + * 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, either version 3 of the License, or + * (at your option) any later version. + * + * 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 for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +#ifndef LIST_SCREEN_H_ +#define LIST_SCREEN_H_ + +#include "texture.h" + +#include +#include +#include + +class ScreenChrome +{ +public: + ScreenChrome(const std::string & optionsLocation, SDL_Renderer * renderer, const std::string & appTitle, + const std::string & version, const std::string & credit); + void SetSectionTitle(const std::string & title); + void Draw(); + +private: + SDL_Renderer * renderer_; + Texture gear_; + Texture appTitle_; + Texture appVersion_; + Texture sectionTitle_; + Texture credit_; +}; + +void DrawFooterDivider(SDL_Renderer * renderer, int rightEdge); + +SDL_Rect DrawListRow(SDL_Renderer * renderer, int index, int rightX, int rowHeight, bool selected); + +class NoteBox +{ +public: + NoteBox(SDL_Renderer * renderer, SDL_Rect rect); + void SetText(const std::string & text); + void Draw(); + +private: + SDL_Renderer * renderer_; + SDL_Rect rect_; + std::vector lines_; +}; + +#endif diff --git a/src/framework/text_layout.cpp b/src/framework/text_layout.cpp index b232ace..e0810c2 100644 --- a/src/framework/text_layout.cpp +++ b/src/framework/text_layout.cpp @@ -77,3 +77,14 @@ std::vector MakeLines(const std::vector & lines, int fontS textures.emplace_back(line, fontSize, renderer, 0, 0, false, ToAbgr(color), true); return textures; } + +void DrawLinesCentered(SDL_Renderer * renderer, std::vector & lines, int centerX, int y, int pitch) +{ + for(Texture & line : lines) + { + line.rect.x = centerX - line.rect.w / 2; + line.rect.y = y; + line.Draw(renderer); + y += pitch; + } +} diff --git a/src/framework/text_layout.h b/src/framework/text_layout.h index e3683f8..8bd0608 100644 --- a/src/framework/text_layout.h +++ b/src/framework/text_layout.h @@ -32,4 +32,6 @@ std::vector WrapToWidth(const std::string & text, int fontSize, int std::vector MakeLines(const std::vector & lines, int fontSize, SDL_Renderer * renderer, Color color); +void DrawLinesCentered(SDL_Renderer * renderer, std::vector & lines, int centerX, int y, int pitch); + #endif diff --git a/src/mod_uninstall.cpp b/src/mod_uninstall.cpp index dca49ad..038a4f8 100644 --- a/src/mod_uninstall.cpp +++ b/src/mod_uninstall.cpp @@ -14,6 +14,7 @@ #include "framework/powerwatch.h" #include "framework/draw_helpers.h" #include "framework/list_jump.h" +#include "framework/list_screen.h" #include "framework/text_layout.h" #include "framework/utf8.h" #include "localization.h" @@ -95,13 +96,8 @@ int main(int argc, char * argv[]) const Color bg = UiTheme::Bg; SetDrawColor(renderer, bg); - Texture gearIcon(OptionsLocation + UiTheme::AssetGear, renderer, UiTheme::GearX, UiTheme::GearY); - Texture appTitleText("OptionsMenu", UiTheme::TitleFontSize, renderer, UiTheme::TitleX, UiTheme::TitleY, false, ToAbgr(UiTheme::Text), true); - appTitleText.rect.y -= appTitleText.rect.h / 2; - Texture appVersionText(MOD_VERSION, UiTheme::VersionFontSize, renderer, appTitleText.rect.x + appTitleText.rect.w + UiTheme::VersionGap, UiTheme::TitleY, false, ToAbgr(UiTheme::Text), true); - appVersionText.rect.y -= appVersionText.rect.h / 2; - Texture titleText(Translate("MODULE_UNINSTALLER"), UiTheme::SectionTitleFontSize, renderer, UiTheme::SectionTitleX, UiTheme::SectionTitleY, false, ToAbgr(UiTheme::Text), true); - Texture creditText("created by CompCom - Modern UI by DefKorns", 16, renderer, UiTheme::CreditX, UiTheme::CreditY, false, ToAbgr(UiTheme::Text), true); + ScreenChrome chrome(OptionsLocation, renderer, "OptionsMenu", MOD_VERSION, "created by CompCom - Modern UI by DefKorns"); + chrome.SetSectionTitle(Translate("MODULE_UNINSTALLER")); const int RowBoxH = UiTheme::RowPitch - 6; Texture checkboxOn(OptionsLocation + UiTheme::AssetCheckboxOn, renderer); @@ -131,15 +127,13 @@ int main(int argc, char * argv[]) const int SummaryRowPitch = 28; const int SummaryMaxRows = (summaryBox.y + summaryBox.h - PanelPad - SummaryListY) / SummaryRowPitch; const int BulletTextGap = 18; - const SDL_Rect noteBox{ UiTheme::DetailX, summaryBox.y + summaryBox.h + 16, UiTheme::DetailW, 72 }; - const int NoteIconCX = noteBox.x + 26; - const int NoteTextX = noteBox.x + 46; + NoteBox rebootNote(renderer, { UiTheme::DetailX, summaryBox.y + summaryBox.h + 16, UiTheme::DetailW, 72 }); + rebootNote.SetText(Translate("MOD_UNINSTALL_REBOOT_NOTE")); Texture summaryHeader(Translate("MOD_UNINSTALLER_TO_REMOVE"), PanelHeaderFontSize, renderer, PanelInnerX, summaryBox.y + 14, false, ToAbgr(UiTheme::TextDim), true); Texture summaryCount; Texture noneMarkedText(Translate("MOD_UNINSTALL_NONE_MARKED"), PanelFontSize, renderer, PanelInnerX, SummaryListY, false, ToAbgr(UiTheme::Text), true); std::vector noneHintLines = MakeLines(WrapToWidth(Translate("MOD_UNINSTALL_NONE_HINT"), PanelFontSize, PanelInnerW), PanelFontSize, renderer, UiTheme::TextDim); - std::vector rebootNoteLines = MakeLines(WrapToWidth(Translate("MOD_UNINSTALL_REBOOT_NOTE"), NoteFontSize, noteBox.x + noteBox.w - PanelPad - NoteTextX), NoteFontSize, renderer, UiTheme::TextDim); Texture moreText; std::vector summaryNames; @@ -158,11 +152,6 @@ int main(int argc, char * argv[]) { return badges.Draw(badge, rightEdgeX, UiTheme::BadgeBandY); }; - auto DrawFooterDivider = [&](int rightEdge) - { - int dividerX = rightEdge - UiTheme::BadgeDividerGapFromCluster; - DrawVLine(renderer, dividerX, UiTheme::FooterY + 10, UiTheme::FooterY + UiTheme::FooterH - 10, UiTheme::Border, 2); - }; int listOffset = 0; int currentId = 0; @@ -197,20 +186,6 @@ int main(int argc, char * argv[]) }; RefreshSummary(); - auto DrawChrome = [&]() - { - DrawStrokeRect(renderer, UiTheme::OuterRect, UiTheme::Border, UiTheme::BorderWidth, UiTheme::BorderRadius); - gearIcon.Draw(renderer); - appTitleText.Draw(renderer); - appVersionText.Draw(renderer); - DrawHLine(renderer, UiTheme::HeaderDividerX, UiTheme::HeaderDividerX + UiTheme::HeaderDividerW, UiTheme::HeaderDividerY, UiTheme::Border, UiTheme::BorderWidth); - DrawHLine(renderer, UiTheme::OuterRect.x, UiTheme::OuterRect.x + UiTheme::OuterRect.w, UiTheme::FooterDividerY, UiTheme::Border, UiTheme::BorderWidth); - int accentBarY = titleText.rect.y + (titleText.rect.h - UiTheme::SectionAccentBarH) / 2; - DrawFillRect(renderer, { UiTheme::SectionTitleX - UiTheme::SectionAccentBarW - 14, accentBarY, UiTheme::SectionAccentBarW, UiTheme::SectionAccentBarH }, UiTheme::Accent); - titleText.Draw(renderer); - creditText.Draw(renderer); - }; - auto DrawList = [&]() { if(mods.empty()) @@ -223,16 +198,8 @@ int main(int argc, char * argv[]) for(int i = 0; i < count; ++i) { Mod & mod = mods[listOffset + i]; - SDL_Rect rowRect{ UiTheme::ListX, UiTheme::RowFirstY + i*UiTheme::RowPitch, UiTheme::ListContentRightX - UiTheme::ListX, RowBoxH }; + SDL_Rect rowRect = DrawListRow(renderer, i, UiTheme::ListContentRightX, RowBoxH, i == currentId); int centerY = rowRect.y + rowRect.h/2; - if(i == currentId) - { - DrawRoundedFillRect(renderer, rowRect, UiTheme::SelectedRowBg, UiTheme::BoxRadius); - DrawStrokeRect(renderer, rowRect, UiTheme::Accent, 2, UiTheme::BoxRadius); - } - else - DrawHLine(renderer, rowRect.x, rowRect.x + rowRect.w, rowRect.y + rowRect.h + 3, UiTheme::Border, 1); - mod.text.rect.y = centerY - mod.text.rect.h/2; SetColorMod(mod.text.texture.get(), mod.marked ? UiTheme::TextDim : UiTheme::Text); mod.text.Draw(renderer); @@ -285,25 +252,12 @@ int main(int argc, char * argv[]) } } - DrawStrokeRect(renderer, noteBox, UiTheme::Border, UiTheme::BorderWidth, UiTheme::BoxRadius); - int noteCenterY = noteBox.y + noteBox.h/2; - DrawStrokeRect(renderer, { NoteIconCX - 9, noteCenterY - 9, 18, 18 }, UiTheme::TextDim, 2, 9); - DrawFillRect(renderer, { NoteIconCX - 1, noteCenterY - 5, 2, 2 }, UiTheme::TextDim); - DrawFillRect(renderer, { NoteIconCX - 1, noteCenterY - 1, 2, 6 }, UiTheme::TextDim); - const int noteLinePitch = 20; - int y = noteCenterY - (static_cast(rebootNoteLines.size()) * noteLinePitch)/2; - for(Texture & line : rebootNoteLines) - { - line.rect.x = NoteTextX; - line.rect.y = y + (noteLinePitch - line.rect.h)/2; - line.Draw(renderer); - y += noteLinePitch; - } + rebootNote.Draw(); }; auto DrawScreen = [&]() { - DrawChrome(); + chrome.Draw(); DrawList(); DrawSummary(); @@ -313,7 +267,7 @@ int main(int argc, char * argv[]) rightEdge = DrawBadge(badgeBack, rightEdge); if(markedCount > 0) rightEdge = DrawBadge(uninstallPill, rightEdge); - DrawFooterDivider(rightEdge); + DrawFooterDivider(renderer, rightEdge); }; auto render = [&]() diff --git a/src/save_manager.cpp b/src/save_manager.cpp index 3f9bdab..b1ba966 100644 --- a/src/save_manager.cpp +++ b/src/save_manager.cpp @@ -15,6 +15,7 @@ #include "framework/powerwatch.h" #include "framework/draw_helpers.h" #include "framework/list_jump.h" +#include "framework/list_screen.h" #include "framework/text_layout.h" #include "framework/utf8.h" #include "localization.h" @@ -152,17 +153,6 @@ namespace { DrawRoundedFillRect(renderer, { x, centerY - 4, 8, 8 }, UiTheme::Accent, 4); } - - void DrawLinesCentered(SDL_Renderer * renderer, std::vector & lines, int centerX, int y, int pitch) - { - for(Texture & line : lines) - { - line.rect.x = centerX - line.rect.w/2; - line.rect.y = y; - line.Draw(renderer); - y += pitch; - } - } } int main(int, char **) @@ -178,16 +168,10 @@ int main(int, char **) const Color bg = UiTheme::Bg; SetDrawColor(renderer, bg); - Texture gearIcon(OptionsLocation + UiTheme::AssetGear, renderer, UiTheme::GearX, UiTheme::GearY); - Texture appTitleText("OptionsMenu", UiTheme::TitleFontSize, renderer, UiTheme::TitleX, UiTheme::TitleY, false, ToAbgr(UiTheme::Text), true); - appTitleText.rect.y -= appTitleText.rect.h / 2; - Texture appVersionText(MOD_VERSION, UiTheme::VersionFontSize, renderer, appTitleText.rect.x + appTitleText.rect.w + UiTheme::VersionGap, UiTheme::TitleY, false, ToAbgr(UiTheme::Text), true); - appVersionText.rect.y -= appVersionText.rect.h / 2; - Texture creditText("Save Manager - by DefKorns", 16, renderer, UiTheme::CreditX, UiTheme::CreditY, false, ToAbgr(UiTheme::Text), true); - Texture titleText; + ScreenChrome chrome(OptionsLocation, renderer, "OptionsMenu", MOD_VERSION, "Save Manager - by DefKorns"); auto SetTitle = [&](const std::string & key) { - titleText = Texture(Translate(key), UiTheme::SectionTitleFontSize, renderer, UiTheme::SectionTitleX, UiTheme::SectionTitleY, false, ToAbgr(UiTheme::Text), true); + chrome.SetSectionTitle(Translate(key)); }; const int RowBoxH = UiTheme::RowPitch - 6; @@ -214,14 +198,12 @@ int main(int, char **) const int SummaryRowPitch = 28; const int SummaryMaxRows = (summaryBox.y + summaryBox.h - PanelPad - SummaryListY) / SummaryRowPitch; const int BulletTextGap = 18; - const SDL_Rect noteBox{ DetailX, summaryBox.y + summaryBox.h + 16, DetailW, 72 }; - const int NoteIconCX = noteBox.x + 26; + NoteBox note(renderer, { DetailX, summaryBox.y + summaryBox.h + 16, DetailW, 72 }); const int StatPitch = 26; const int StatsBoxH = 4*StatPitch + 2*PanelPad - 6; const int DetailBottomY = UiTheme::FooterDividerY - UiTheme::ListBottomMargin; const SDL_Rect statsBox{ DetailX, DetailBottomY - StatsBoxH, DetailW, StatsBoxH }; const SDL_Rect descriptionBox{ DetailX, UiTheme::RowFirstY, DetailW, statsBox.y - 16 - UiTheme::RowFirstY }; - const int NoteTextX = noteBox.x + 46; BadgePainter badges(OptionsLocation, renderer); auto MakeBadge = [&](const std::string & letter, const std::string & labelKey, Color rim, Color fill) -> Badge @@ -239,62 +221,6 @@ int main(int, char **) { return badges.Draw(badge, rightEdgeX, UiTheme::BadgeBandY); }; - auto DrawFooterDivider = [&](int rightEdge) - { - int dividerX = rightEdge - UiTheme::BadgeDividerGapFromCluster; - DrawVLine(renderer, dividerX, UiTheme::FooterY + 10, UiTheme::FooterY + UiTheme::FooterH - 10, UiTheme::Border, 2); - }; - - auto DrawChrome = [&]() - { - DrawStrokeRect(renderer, UiTheme::OuterRect, UiTheme::Border, UiTheme::BorderWidth, UiTheme::BorderRadius); - gearIcon.Draw(renderer); - appTitleText.Draw(renderer); - appVersionText.Draw(renderer); - DrawHLine(renderer, UiTheme::HeaderDividerX, UiTheme::HeaderDividerX + UiTheme::HeaderDividerW, UiTheme::HeaderDividerY, UiTheme::Border, UiTheme::BorderWidth); - DrawHLine(renderer, UiTheme::OuterRect.x, UiTheme::OuterRect.x + UiTheme::OuterRect.w, UiTheme::FooterDividerY, UiTheme::Border, UiTheme::BorderWidth); - int accentBarY = titleText.rect.y + (titleText.rect.h - UiTheme::SectionAccentBarH) / 2; - DrawFillRect(renderer, { UiTheme::SectionTitleX - UiTheme::SectionAccentBarW - 14, accentBarY, UiTheme::SectionAccentBarW, UiTheme::SectionAccentBarH }, UiTheme::Accent); - titleText.Draw(renderer); - creditText.Draw(renderer); - }; - - auto DrawRowFrame = [&](int index, bool selected) -> SDL_Rect - { - SDL_Rect rowRect{ UiTheme::ListX, UiTheme::RowFirstY + index*UiTheme::RowPitch, ListRightX - UiTheme::ListX, RowBoxH }; - if(selected) - { - DrawRoundedFillRect(renderer, rowRect, UiTheme::SelectedRowBg, UiTheme::BoxRadius); - DrawStrokeRect(renderer, rowRect, UiTheme::Accent, 2, UiTheme::BoxRadius); - } - else - DrawHLine(renderer, rowRect.x, rowRect.x + rowRect.w, rowRect.y + rowRect.h + 3, UiTheme::Border, 1); - return rowRect; - }; - - auto DrawNote = [&](std::vector & lines) - { - DrawStrokeRect(renderer, noteBox, UiTheme::Border, UiTheme::BorderWidth, UiTheme::BoxRadius); - int noteCenterY = noteBox.y + noteBox.h/2; - DrawStrokeRect(renderer, { NoteIconCX - 9, noteCenterY - 9, 18, 18 }, UiTheme::TextDim, 2, 9); - DrawFillRect(renderer, { NoteIconCX - 1, noteCenterY - 5, 2, 2 }, UiTheme::TextDim); - DrawFillRect(renderer, { NoteIconCX - 1, noteCenterY - 1, 2, 6 }, UiTheme::TextDim); - const int noteLinePitch = 20; - int y = noteCenterY - (static_cast(lines.size()) * noteLinePitch)/2; - for(Texture & line : lines) - { - line.rect.x = NoteTextX; - line.rect.y = y + (noteLinePitch - line.rect.h)/2; - line.Draw(renderer); - y += noteLinePitch; - } - }; - - auto NoteLines = [&](const std::string & key) - { - return MakeLines(WrapToWidth(Translate(key), NoteFontSize, noteBox.x + noteBox.w - PanelPad - NoteTextX), NoteFontSize, renderer, UiTheme::TextDim); - }; - // confirm (Start + Select) or acknowledge (A); B cancels auto RunDialog = [&](const std::function & drawUnder, const std::string & titleKey, const std::vector & bodyLines, const std::string & noteKey, bool confirm) -> bool @@ -398,7 +324,7 @@ int main(int, char **) Texture summaryHeader(Translate(list.headerKey), PanelHeaderFontSize, renderer, PanelInnerX, summaryBox.y + 14, false, ToAbgr(UiTheme::TextDim), true); Texture noneMarkedText(Translate("SAVE_MANAGER_NONE_MARKED"), PanelFontSize, renderer, PanelInnerX, SummaryListY, false, ToAbgr(UiTheme::Text), true); std::vector noneHintLines = MakeLines(WrapToWidth(Translate("SAVE_MANAGER_NONE_HINT"), PanelFontSize, PanelInnerW), PanelFontSize, renderer, UiTheme::TextDim); - std::vector noteLines = NoteLines(list.noteKey); + note.SetText(Translate(list.noteKey)); Badge actionPill = badges.MakePill("Start", ""); GameNames gameNames = LoadGameNames(); @@ -475,14 +401,14 @@ int main(int, char **) auto DrawScreen = [&]() { - DrawChrome(); + chrome.Draw(); const int entryCount = static_cast(entries.size()); const int rowCount = entryCount + selectAllRows; const int count = std::min(displayCount, rowCount - listOffset); for(int i = 0; i < count; ++i) { const int row = listOffset + i; - SDL_Rect rowRect = DrawRowFrame(i, i == currentId); + SDL_Rect rowRect = DrawListRow(renderer, i, ListRightX, RowBoxH, i == currentId); int centerY = rowRect.y + rowRect.h/2; Texture & text = row == 0 ? selectAllText : entries[row - selectAllRows].text; const bool marked = row == 0 ? markedCount == entryCount : entries[row - selectAllRows].marked; @@ -532,7 +458,7 @@ int main(int, char **) moreText.Draw(renderer); } } - DrawNote(noteLines); + note.Draw(); int rightEdge = UiTheme::BadgeClusterRightX; const int row = listOffset + currentId; @@ -541,7 +467,7 @@ int main(int, char **) rightEdge = DrawBadge(badgeBack, rightEdge); if(markedCount > 0) rightEdge = DrawBadge(actionPill, rightEdge); - DrawFooterDivider(rightEdge); + DrawFooterDivider(renderer, rightEdge); }; auto Labels = [&]() @@ -699,10 +625,10 @@ int main(int, char **) auto DrawActions = [&]() { - DrawChrome(); + chrome.Draw(); for(int i = 0; i < static_cast(Actions.size()); ++i) { - SDL_Rect rowRect = DrawRowFrame(i, i == currentAction); + SDL_Rect rowRect = DrawListRow(renderer, i, ListRightX, RowBoxH, i == currentAction); int centerY = rowRect.y + rowRect.h/2; actionTexts[i].rect.y = centerY - actionTexts[i].rect.h/2; actionTexts[i].Draw(renderer); @@ -742,7 +668,7 @@ int main(int, char **) int rightEdge = UiTheme::BadgeClusterRightX; rightEdge = DrawBadge(badgeSelect, rightEdge); rightEdge = DrawBadge(badgeBack, rightEdge); - DrawFooterDivider(rightEdge); + DrawFooterDivider(renderer, rightEdge); }; auto ShowMessage = [&](const std::string & titleKey, const std::string & noteKey) From 6528693797b1ccc93db78882195d36de97d0c000 Mon Sep 17 00:00:00 2001 From: DefKorns Date: Wed, 21 Oct 2026 08:41:05 +0000 Subject: [PATCH 2/7] Run the Save Manager and Uninstaller dialogs through PanelDialog --- Makefile | 2 +- src/framework/panel_dialog.cpp | 187 +++++++++++++++++++++++++++++++++ src/framework/panel_dialog.h | 78 ++++++++++++++ src/mod_uninstall.cpp | 96 ++++------------- src/save_manager.cpp | 136 ++++++------------------ 5 files changed, 318 insertions(+), 181 deletions(-) create mode 100644 src/framework/panel_dialog.cpp create mode 100644 src/framework/panel_dialog.h diff --git a/Makefile b/Makefile index bf8edcb..e456cdd 100644 --- a/Makefile +++ b/Makefile @@ -36,7 +36,7 @@ VENDORED_LIBS = endif CXXFLAGS = -std=c++14 -Os $(SDL_CFLAGS) -DMOD_VERSION=\"v$(MOD_VER)\" LDLIBS = $(SDL_LIBS) -SOURCES = src/main.cpp src/back_stack.cpp src/command.cpp src/download_screen.cpp src/localization.cpp src/framework/badge.cpp src/framework/dialog.cpp src/framework/sdl_context.cpp src/framework/texture.cpp src/framework/controller.cpp src/framework/powerwatch.cpp src/framework/draw_helpers.cpp src/framework/utf8.cpp src/framework/font8x8_lookup.cpp src/framework/uitheme.cpp src/framework/list_jump.cpp src/framework/text_layout.cpp src/framework/list_screen.cpp +SOURCES = src/main.cpp src/back_stack.cpp src/command.cpp src/download_screen.cpp src/localization.cpp src/framework/badge.cpp src/framework/dialog.cpp src/framework/sdl_context.cpp src/framework/texture.cpp src/framework/controller.cpp src/framework/powerwatch.cpp src/framework/draw_helpers.cpp src/framework/utf8.cpp src/framework/font8x8_lookup.cpp src/framework/uitheme.cpp src/framework/list_jump.cpp src/framework/text_layout.cpp src/framework/list_screen.cpp src/framework/panel_dialog.cpp OBJECTS = $(SOURCES:.cpp=.o) ALL_OBJECTS = $(OBJECTS) src/daemon.o src/mod_uninstall.o src/save_manager.o src/standby_watchdog.o src/gen_splash.o src/change_combo.o diff --git a/src/framework/panel_dialog.cpp b/src/framework/panel_dialog.cpp new file mode 100644 index 0000000..cfc0432 --- /dev/null +++ b/src/framework/panel_dialog.cpp @@ -0,0 +1,187 @@ +/** + * Copyright (c) 2026 DefKorns (https://defkorns.github.io/LICENSE) + * + * 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, either version 3 of the License, or + * (at your option) any later version. + * + * 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 for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +#include "panel_dialog.h" +#include "controller.h" +#include "dialog.h" +#include "draw_helpers.h" +#include "powerwatch.h" +#include "sdl_context.h" +#include "text_layout.h" +#include "uitheme.h" + +#include +#include + +namespace +{ + constexpr int BoxW = 600; + constexpr int BaseH = 110; + constexpr int TitleTopY = 36; + constexpr int TitlePitch = 32; + constexpr int WrapInset = 48; + constexpr int BodyTopY = 72; + constexpr int BodyFontSize = 16; + constexpr int BodyPitch = 30; + constexpr int BodyInset = 80; + constexpr int BulletSize = 8; + constexpr int BulletTextGap = 18; + constexpr int NoteFontSize = 15; + constexpr int NotePitch = 20; + constexpr int NoteGap = 6; + + constexpr int BusyMinW = 420; + constexpr int BusyPadW = 96; + constexpr int BusyH = 110; + constexpr int BusyTextLift = 12; + + DialogResult Finish(Controller & controller, DialogResult result) + { + DropPendingInput(controller); + return result; + } +} + +PanelDialog::PanelDialog(SDL_Renderer * renderer, BadgePainter & painter, Kind kind, const PanelDialogText & text, std::vector badges) + : renderer_(renderer) + , painter_(&painter) + , kind_(kind) + , text_(text) + , badges_(std::move(badges)) +{ +} + +void PanelDialog::SetBulletColor(Color color) +{ + bullets_ = true; + bulletColor_ = color; +} + +void PanelDialog::Layout() +{ + const int bodyMaxW = BoxW - BodyInset - (bullets_ ? BulletTextGap : 0); + body_.clear(); + for(const std::string & line : text_.body) + body_.emplace_back(TruncateToWidth(line, BodyFontSize, bodyMaxW), BodyFontSize, renderer_, 0, 0, false, ToAbgr(UiTheme::Text), true); + bulletRows_ = static_cast(body_.size()); + if(text_.hiddenCount > 0) + body_.emplace_back("+" + std::to_string(text_.hiddenCount), BodyFontSize, renderer_, 0, 0, false, ToAbgr(UiTheme::TextDim), true); + + note_.clear(); + if(!text_.note.empty()) + note_ = MakeLines(WrapToWidth(text_.note, NoteFontSize, BoxW - WrapInset), NoteFontSize, renderer_, UiTheme::TextDim); + + title_.clear(); + for(const std::string & line : WrapToWidth(text_.title, Dialog::TitleFontSize, BoxW - WrapInset)) + title_.emplace_back(line, Dialog::TitleFontSize, renderer_, 0, 0, false, ToAbgr(UiTheme::Text), true); + titleExtraH_ = std::max(0, static_cast(title_.size()) - 1) * TitlePitch; + + const int boxH = BaseH + titleExtraH_ + static_cast(body_.size()) * BodyPitch + static_cast(note_.size()) * NotePitch + + Dialog::BadgeRowGap + UiTheme::BadgeOuterSize; + box_ = Dialog::PanelRect(BoxW, boxH); + const int centerX = box_.x + box_.w / 2; + for(size_t i = 0; i < title_.size(); ++i) + { + title_[i].rect.x = centerX - title_[i].rect.w / 2; + title_[i].rect.y = box_.y + TitleTopY + static_cast(i) * TitlePitch - title_[i].rect.h / 2; + } + + int widest = 0; + for(int i = 0; i < bulletRows_; ++i) + widest = std::max(widest, body_[i].rect.w); + bulletX_ = centerX - (widest + BulletTextGap) / 2; +} + +void PanelDialog::Draw() +{ + Dialog::DrawBackdrop(renderer_); + Dialog::DrawPanel(renderer_, box_); + for(Texture & line : title_) + line.Draw(renderer_); + + const int centerX = box_.x + box_.w / 2; + int y = box_.y + BodyTopY + titleExtraH_; + for(int i = 0; i < static_cast(body_.size()); ++i) + { + Texture & line = body_[i]; + if(bullets_) + { + if(i < bulletRows_) + DrawRoundedFillRect(renderer_, { bulletX_, y + line.rect.h / 2 - BulletSize / 2, BulletSize, BulletSize }, bulletColor_, BulletSize / 2); + line.rect.x = bulletX_ + BulletTextGap; + } + else + line.rect.x = centerX - line.rect.w / 2; + line.rect.y = y; + line.Draw(renderer_); + y += BodyPitch; + } + DrawLinesCentered(renderer_, note_, centerX, y + NoteGap, NotePitch); + painter_->DrawCentered(badges_, centerX, box_.y + box_.h - Dialog::BottomPad - UiTheme::BadgeOuterSize); +} + +DialogResult PanelDialog::Run(SDL_Context & sdl, Controller & controller, const std::function & drawUnder) +{ + Layout(); + DropPendingInput(controller); + for(;;) + { + bool quit = false; + SDL_Event e; + while(SDL_PollEvent(&e)) + quit = quit || e.type == SDL_QUIT; + controller.Update(); + if(quit || sdl.powerwatch->buttonPress()) + return Finish(controller, DialogResult::Quit); + if(kind_ == Kind::Confirm) + { + if(controller.GetButtonStatus(B)) + return Finish(controller, DialogResult::Cancelled); + if(controller.PeekButtonStatus(START) && controller.PeekButtonStatus(SELECT)) + return Finish(controller, DialogResult::Confirmed); + } + else if(controller.GetButtonStatus(A)) + return Finish(controller, DialogResult::Confirmed); + + sdl.StartFrame(); + drawUnder(); + Draw(); + SetDrawColor(renderer_, UiTheme::Bg); + sdl.EndFrame(); + } +} + +void ShowBusy(SDL_Context & sdl, const std::function & drawUnder, const std::string & message) +{ + SDL_Renderer * renderer = sdl.renderer; + const SDL_Rect box = Dialog::PanelRect(std::max(BusyMinW, TextWidth(message, Dialog::TitleFontSize) + BusyPadW), BusyH); + Texture text(message, Dialog::TitleFontSize, renderer, box.x + box.w / 2, box.y + box.h / 2 - BusyTextLift, true, ToAbgr(UiTheme::Text), true); + sdl.StartFrame(); + drawUnder(); + Dialog::DrawBackdrop(renderer); + Dialog::DrawPanel(renderer, box); + text.Draw(renderer); + SetDrawColor(renderer, UiTheme::Bg); + sdl.EndFrame(); +} + +// Update() first: presses queued in evdev during blocking work would otherwise land on the next frame. +void DropPendingInput(Controller & controller) +{ + controller.Update(); + controller.Reset(); +} diff --git a/src/framework/panel_dialog.h b/src/framework/panel_dialog.h new file mode 100644 index 0000000..f566997 --- /dev/null +++ b/src/framework/panel_dialog.h @@ -0,0 +1,78 @@ +/** + * Copyright (c) 2026 DefKorns (https://defkorns.github.io/LICENSE) + * + * 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, either version 3 of the License, or + * (at your option) any later version. + * + * 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 for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +#ifndef PANEL_DIALOG_H_ +#define PANEL_DIALOG_H_ + +#include "badge.h" +#include "color.h" +#include "texture.h" + +#include +#include +#include + +struct SDL_Context; +class Controller; + +enum class DialogResult { Confirmed, Cancelled, Quit }; + +struct PanelDialogText +{ + std::string title; + std::vector body; + int hiddenCount = 0; + std::string note; +}; + +// Acknowledge closes on A only; Confirm needs Start + Select held, B cancels. +class PanelDialog +{ +public: + enum class Kind { Acknowledge, Confirm }; + + PanelDialog(SDL_Renderer * renderer, BadgePainter & painter, Kind kind, const PanelDialogText & text, std::vector badges); + void SetBulletColor(Color color); + DialogResult Run(SDL_Context & sdl, Controller & controller, const std::function & drawUnder); + +private: + void Layout(); + void Draw(); + + SDL_Renderer * renderer_; + BadgePainter * painter_; + Kind kind_; + PanelDialogText text_; + std::vector badges_; + bool bullets_ = false; + Color bulletColor_{ 0, 0, 0 }; + + std::vector title_; + int titleExtraH_ = 0; + std::vector body_; + int bulletRows_ = 0; + int bulletX_ = 0; + std::vector note_; + SDL_Rect box_{ 0, 0, 0, 0 }; +}; + +// One frame only; the caller runs the blocking work right after. +void ShowBusy(SDL_Context & sdl, const std::function & drawUnder, const std::string & message); + +void DropPendingInput(Controller & controller); + +#endif diff --git a/src/mod_uninstall.cpp b/src/mod_uninstall.cpp index 038a4f8..e204b5b 100644 --- a/src/mod_uninstall.cpp +++ b/src/mod_uninstall.cpp @@ -8,13 +8,13 @@ */ #include "framework/badge.h" -#include "framework/dialog.h" #include "framework/sdl_helper.h" #include "framework/controller.h" #include "framework/powerwatch.h" #include "framework/draw_helpers.h" #include "framework/list_jump.h" #include "framework/list_screen.h" +#include "framework/panel_dialog.h" #include "framework/text_layout.h" #include "framework/utf8.h" #include "localization.h" @@ -32,7 +32,6 @@ namespace const int ListFontSize = 16; const int PanelFontSize = 16; const int PanelHeaderFontSize = 18; - const int NoteFontSize = 15; const int ExitBack = 0; const int ExitLeaveMenu = 1; @@ -278,90 +277,30 @@ int main(int argc, char * argv[]) sdl_context.EndFrame(); }; - auto ConfirmUninstall = [&]() -> bool + auto ConfirmUninstall = [&]() { auto marked = MarkedMods(); const int maxNames = 8; - const int namePitch = 30; - const int shown = std::min(maxNames, static_cast(marked.size())); - const bool overflow = static_cast(marked.size()) > shown; - const int listRows = shown + (overflow ? 1 : 0); - - const int boxW = 560; - const int badgeRowH = Dialog::BadgeRowGap + UiTheme::BadgeOuterSize; - const int boxH = 150 + listRows*namePitch + badgeRowH; - const SDL_Rect box = Dialog::PanelRect(boxW, boxH); - - Texture confirmTitle(Translate("MOD_UNINSTALL_CONFIRM"), 24, renderer, 640, box.y + 36, true, ToAbgr(UiTheme::Text), true); - std::vector names; - int nameW = 0; - for(int i = 0; i < shown; ++i) - { - names.emplace_back(TruncateToWidth(marked[i]->name, PanelFontSize, boxW - 80), PanelFontSize, renderer, 0, 0, false, ToAbgr(UiTheme::Text), true); - nameW = std::max(nameW, names.back().rect.w); - } - if(overflow) - names.emplace_back("+" + std::to_string(marked.size() - shown), PanelFontSize, renderer, 0, 0, false, ToAbgr(UiTheme::TextDim), true); - const int listX = 640 - (nameW + BulletTextGap)/2; - std::vector noteLines = MakeLines(WrapToWidth(Translate("MOD_UNINSTALL_REBOOT_NOTE"), NoteFontSize, boxW - 48), NoteFontSize, renderer, UiTheme::TextDim); - - controller.GetButtonStatus(B); - bool confirmed = false; - for(;;) - { - SDL_Event e; - while(SDL_PollEvent(&e)) - if(e.type == SDL_QUIT) - return false; - controller.Update(); - if(sdl_context.powerwatch->buttonPress() || controller.GetButtonStatus(B)) - break; - if(controller.PeekButtonStatus(START) && controller.PeekButtonStatus(SELECT)) - { - confirmed = true; - break; - } - - sdl_context.StartFrame(); - DrawScreen(); - Dialog::DrawBackdrop(renderer); - Dialog::DrawPanel(renderer, box); - confirmTitle.Draw(renderer); - int y = box.y + 78; - for(int i = 0; i < static_cast(names.size()); ++i) - { - if(i < shown) - DrawDot(renderer, listX, y); - names[i].rect.x = listX + BulletTextGap; - names[i].rect.y = y - names[i].rect.h/2; - names[i].Draw(renderer); - y += namePitch; - } - int noteY = box.y + box.h - badgeRowH - 34 - (static_cast(noteLines.size()) - 1) * 10; - for(Texture & line : noteLines) - { - line.rect.x = 640 - line.rect.w/2; - line.rect.y = noteY - line.rect.h/2; - line.Draw(renderer); - noteY += 20; - } - - badges.DrawCentered({ &badgeCancel, &confirmPill }, box.x + box.w / 2, box.y + box.h - Dialog::BottomPad - UiTheme::BadgeOuterSize); - - SetDrawColor(renderer, bg); - sdl_context.EndFrame(); - } - if(!confirmed) - return false; + PanelDialogText text; + text.title = Translate("MOD_UNINSTALL_CONFIRM"); + for(int i = 0; i < std::min(maxNames, static_cast(marked.size())); ++i) + text.body.push_back(marked[i]->name); + text.hiddenCount = std::max(0, static_cast(marked.size()) - maxNames); + text.note = Translate("MOD_UNINSTALL_REBOOT_NOTE"); + PanelDialog dialog(renderer, badges, PanelDialog::Kind::Confirm, text, { &badgeCancel, &confirmPill }); + dialog.SetBulletColor(UiTheme::CheckboxOn); + const DialogResult answer = dialog.Run(sdl_context, controller, DrawScreen); + if(answer != DialogResult::Confirmed) + return answer; std::ofstream out("/tmp/uninstall"); if(!out.is_open()) - return false; + return DialogResult::Cancelled; for(const Mod * mod : marked) out << mod->name << " "; out.close(); system(("/bin/sh " + folderLocation + "/FinishUninstall.sh &").c_str()); - return true; + return DialogResult::Confirmed; }; ListJumpInput jumpInput; @@ -408,7 +347,10 @@ int main(int argc, char * argv[]) return ExitBack; else if(controller.GetButtonStatus(START) && markedCount > 0) { - if(ConfirmUninstall()) + const DialogResult answer = ConfirmUninstall(); + if(answer == DialogResult::Quit) + return ExitLeaveMenu; + if(answer == DialogResult::Confirmed) return ExitUninstallStarted; } diff --git a/src/save_manager.cpp b/src/save_manager.cpp index b1ba966..736f7fe 100644 --- a/src/save_manager.cpp +++ b/src/save_manager.cpp @@ -9,13 +9,13 @@ */ #include "framework/badge.h" -#include "framework/dialog.h" #include "framework/sdl_helper.h" #include "framework/controller.h" #include "framework/powerwatch.h" #include "framework/draw_helpers.h" #include "framework/list_jump.h" #include "framework/list_screen.h" +#include "framework/panel_dialog.h" #include "framework/text_layout.h" #include "framework/utf8.h" #include "localization.h" @@ -47,7 +47,6 @@ namespace const int ListFontSize = 16; const int PanelFontSize = 16; const int PanelHeaderFontSize = 18; - const int NoteFontSize = 15; const int ExitBack = 0; const int ExitLeaveMenu = 1; @@ -221,100 +220,23 @@ int main(int, char **) { return badges.Draw(badge, rightEdgeX, UiTheme::BadgeBandY); }; - // confirm (Start + Select) or acknowledge (A); B cancels - auto RunDialog = [&](const std::function & drawUnder, const std::string & titleKey, const std::vector & bodyLines, - const std::string & noteKey, bool confirm) -> bool + auto Confirm = [&](const std::function & drawUnder, const PanelDialogText & text) { - const int boxW = 600; - const int bodyPitch = 30; - const int badgeRowH = Dialog::BadgeRowGap + UiTheme::BadgeOuterSize; - std::vector body; - for(const std::string & line : bodyLines) - body.emplace_back(TruncateToWidth(line, PanelFontSize, boxW - 80), PanelFontSize, renderer, 0, 0, false, ToAbgr(UiTheme::Text), true); - std::vector noteLines = noteKey.empty() ? std::vector() - : MakeLines(WrapToWidth(Translate(noteKey), NoteFontSize, boxW - 48), NoteFontSize, renderer, UiTheme::TextDim); - const int titlePitch = 32; - std::vector titleLines; - for(const std::string & line : WrapToWidth(Translate(titleKey), Dialog::TitleFontSize, boxW - 48)) - titleLines.emplace_back(line, Dialog::TitleFontSize, renderer, 640, 0, true, ToAbgr(UiTheme::Text), true); - const int titleExtraH = std::max(0, static_cast(titleLines.size()) - 1) * titlePitch; - const int boxH = 110 + titleExtraH + static_cast(body.size())*bodyPitch + static_cast(noteLines.size())*20 + badgeRowH; - const SDL_Rect box = Dialog::PanelRect(boxW, boxH); - for(size_t i = 0; i < titleLines.size(); ++i) - titleLines[i].rect.y = box.y + 36 + static_cast(i)*titlePitch - titleLines[i].rect.h/2; - - controller.GetButtonStatus(A); - controller.GetButtonStatus(B); - for(;;) - { - SDL_Event e; - while(SDL_PollEvent(&e)) - if(e.type == SDL_QUIT) - return false; - controller.Update(); - if(sdl_context.powerwatch->buttonPress() || controller.GetButtonStatus(B)) - return false; - if(confirm && controller.PeekButtonStatus(START) && controller.PeekButtonStatus(SELECT)) - { - // still held on return - controller.GetButtonStatus(START); - controller.GetButtonStatus(SELECT); - return true; - } - if(!confirm && controller.GetButtonStatus(A)) - return true; - - sdl_context.StartFrame(); - drawUnder(); - Dialog::DrawBackdrop(renderer); - Dialog::DrawPanel(renderer, box); - for(Texture & line : titleLines) - line.Draw(renderer); - int y = box.y + 72 + titleExtraH; - for(Texture & line : body) - { - line.rect.x = 640 - line.rect.w/2; - line.rect.y = y; - line.Draw(renderer); - y += bodyPitch; - } - DrawLinesCentered(renderer, noteLines, 640, y + 6, 20); - if(confirm) - badges.DrawCentered({ &badgeCancel, &confirmPill }, box.x + box.w / 2, box.y + box.h - Dialog::BottomPad - UiTheme::BadgeOuterSize); - else - badges.DrawCentered({ &badgeOk }, box.x + box.w / 2, box.y + box.h - Dialog::BottomPad - UiTheme::BadgeOuterSize); - SetDrawColor(renderer, bg); - sdl_context.EndFrame(); - } + return PanelDialog(renderer, badges, PanelDialog::Kind::Confirm, text, { &badgeCancel, &confirmPill }).Run(sdl_context, controller, drawUnder); }; - // Shows a progress panel for one frame; the caller then runs the blocking work. - auto ShowBusy = [&](const std::function & drawUnder, const std::string & messageKey) + auto ShowMessage = [&](const std::function & drawUnder, const std::string & titleKey) { - const std::string message = Translate(messageKey); - const SDL_Rect box = Dialog::PanelRect(std::max(420, TextWidth(message, Dialog::TitleFontSize) + 96), 110); - Texture working(message, Dialog::TitleFontSize, renderer, 640, box.y + box.h/2 - 12, true, ToAbgr(UiTheme::Text), true); - sdl_context.StartFrame(); - drawUnder(); - Dialog::DrawBackdrop(renderer); - Dialog::DrawPanel(renderer, box); - working.Draw(renderer); - SetDrawColor(renderer, bg); - sdl_context.EndFrame(); - }; - - auto DropPendingInput = [&]() - { - controller.Update(); - controller.GetButtonStatus(A); - controller.GetButtonStatus(B); + PanelDialogText text; + text.title = Translate(titleKey); + return PanelDialog(renderer, badges, PanelDialog::Kind::Acknowledge, text, { &badgeOk }).Run(sdl_context, controller, drawUnder); }; auto RunBusy = [&](const std::function & drawUnder, const std::string & command) -> bool { - ShowBusy(drawUnder, "SAVE_MANAGER_WORKING"); + ShowBusy(sdl_context, drawUnder, Translate("SAVE_MANAGER_WORKING")); const int status = system(command.c_str()); - DropPendingInput(); + DropPendingInput(controller); return status != -1 && WIFEXITED(status) && WEXITSTATUS(status) == 0; }; @@ -530,13 +452,17 @@ int main(int, char **) else if(controller.GetButtonStatus(START) && markedCount > 0) { auto marked = Marked(); - std::vector lines; const int maxNames = 6; + PanelDialogText text; + text.title = Translate(list.confirmKey); for(int i = 0; i < std::min(maxNames, static_cast(marked.size())); ++i) - lines.push_back(marked[i]->label); - if(static_cast(marked.size()) > maxNames) - lines.push_back("+" + std::to_string(marked.size() - maxNames)); - if(RunDialog(DrawScreen, list.confirmKey, lines, list.noteKey, true)) + text.body.push_back(marked[i]->label); + text.hiddenCount = std::max(0, static_cast(marked.size()) - maxNames); + text.note = Translate(list.noteKey); + const DialogResult answer = Confirm(DrawScreen, text); + if(answer == DialogResult::Quit) + return ExitLeaveMenu; + if(answer == DialogResult::Confirmed) { const bool fromBackup = &list == &RestoreBackupList; bool ok; @@ -553,8 +479,8 @@ int main(int, char **) std::remove(SelectionFile.c_str()); } Load(); - if(fromBackup) - RunDialog(DrawScreen, ok ? "RESTORE_SAVES_USB_DONE" : "RESTORE_SAVES_USB_FAILED", {}, "", false); + if(fromBackup && ShowMessage(DrawScreen, ok ? "RESTORE_SAVES_USB_DONE" : "RESTORE_SAVES_USB_FAILED") == DialogResult::Quit) + return ExitLeaveMenu; // an empty list would still show a checked "select all" if(entries.empty()) return ExitBack; @@ -671,23 +597,27 @@ int main(int, char **) DrawFooterDivider(renderer, rightEdge); }; - auto ShowMessage = [&](const std::string & titleKey, const std::string & noteKey) - { - RunDialog(DrawActions, titleKey, {}, noteKey, false); - }; - auto RunAction = [&](Action action) -> int { switch(action) { case Action::Backup: - if(RunDialog(DrawActions, "SAVE_MANAGER_BACKUP_CONFIRM", {}, "SAVE_MANAGER_BACKUP_DESC", true)) + { + PanelDialogText text; + text.title = Translate("SAVE_MANAGER_BACKUP_CONFIRM"); + text.note = Translate("SAVE_MANAGER_BACKUP_DESC"); + const DialogResult answer = Confirm(DrawActions, text); + if(answer == DialogResult::Quit) + return ExitLeaveMenu; + if(answer == DialogResult::Confirmed) { const bool ok = RunBusy(DrawActions, "sh " + ScriptLocation + "BackupSavesUsb.sh >/dev/null 2>&1"); RefreshStats(); - ShowMessage(ok ? "BACKUP_SAVES_USB_DONE" : "BACKUP_SAVES_USB_FAILED", ""); + if(ShowMessage(DrawActions, ok ? "BACKUP_SAVES_USB_DONE" : "BACKUP_SAVES_USB_FAILED") == DialogResult::Quit) + return ExitLeaveMenu; } return -1; + } case Action::RestoreUsb: case Action::Clean: case Action::RestoreDeleted: @@ -695,8 +625,8 @@ int main(int, char **) const int available = action == Action::RestoreUsb ? stats.backupSaves : action == Action::Clean ? stats.orphans : stats.deleted; if(available <= 0) { - ShowMessage(action == Action::RestoreUsb ? "RESTORE_SAVES_USB_NOT_FOUND" : action == Action::Clean ? "SAVE_MANAGER_NO_ORPHANS" : "SAVE_MANAGER_NO_DELETED", ""); - return -1; + const std::string titleKey = action == Action::RestoreUsb ? "RESTORE_SAVES_USB_NOT_FOUND" : action == Action::Clean ? "SAVE_MANAGER_NO_ORPHANS" : "SAVE_MANAGER_NO_DELETED"; + return ShowMessage(DrawActions, titleKey) == DialogResult::Quit ? ExitLeaveMenu : -1; } const int result = RunSaveList(action == Action::RestoreUsb ? RestoreBackupList : action == Action::Clean ? CleanList : RestoreDeletedList); SetTitle("SAVE_MANAGER"); From c44579c96b6c13727147fb5d5ef0edab33de00d5 Mon Sep 17 00:00:00 2001 From: DefKorns Date: Wed, 21 Oct 2026 08:55:13 +0000 Subject: [PATCH 3/7] Draw the list scroll arrows through the framework --- src/framework/list_screen.cpp | 18 ++++++++++++++++++ src/framework/list_screen.h | 12 ++++++++++++ src/framework/uitheme.h | 5 ++++- src/main.cpp | 12 +++--------- src/mod_uninstall.cpp | 10 ++-------- src/save_manager.cpp | 10 ++-------- 6 files changed, 41 insertions(+), 26 deletions(-) diff --git a/src/framework/list_screen.cpp b/src/framework/list_screen.cpp index 46eed27..15b6a9b 100644 --- a/src/framework/list_screen.cpp +++ b/src/framework/list_screen.cpp @@ -87,6 +87,24 @@ SDL_Rect DrawListRow(SDL_Renderer * renderer, int index, int rightX, int rowHeig return rowRect; } +ScrollArrows::ScrollArrows(const std::string & optionsLocation, SDL_Renderer * renderer) + : renderer_(renderer) + , up_(optionsLocation + UiTheme::AssetChevronUp, renderer, UiTheme::ScrollX, UiTheme::ScrollUpY) +{ + SetColorMod(up_.texture.get(), UiTheme::ScrollArrow); + up_.rect.x -= up_.rect.w / 2; + down_ = up_; + down_.rect.y = UiTheme::ScrollDownY; +} + +void ScrollArrows::Draw(bool moreAbove, bool moreBelow) +{ + if(moreAbove) + up_.Draw(renderer_); + if(moreBelow) + down_.Draw(renderer_, SDL_FLIP_VERTICAL); +} + NoteBox::NoteBox(SDL_Renderer * renderer, SDL_Rect rect) : renderer_(renderer) , rect_(rect) diff --git a/src/framework/list_screen.h b/src/framework/list_screen.h index ef68e12..1debea3 100644 --- a/src/framework/list_screen.h +++ b/src/framework/list_screen.h @@ -45,6 +45,18 @@ void DrawFooterDivider(SDL_Renderer * renderer, int rightEdge); SDL_Rect DrawListRow(SDL_Renderer * renderer, int index, int rightX, int rowHeight, bool selected); +class ScrollArrows +{ +public: + ScrollArrows(const std::string & optionsLocation, SDL_Renderer * renderer); + void Draw(bool moreAbove, bool moreBelow); + +private: + SDL_Renderer * renderer_; + Texture up_; + Texture down_; +}; + class NoteBox { public: diff --git a/src/framework/uitheme.h b/src/framework/uitheme.h index f95c9ae..193af81 100644 --- a/src/framework/uitheme.h +++ b/src/framework/uitheme.h @@ -120,8 +120,11 @@ namespace UiTheme const int CreditY = FooterY + FooterH/2 - 8; const int ScrollX = FrameX + (ListX - FrameX) / 2 - 7; + const int ScrollArrowH = 24; + const int ListRows = (FooterDividerY - ListBottomMargin - RowFirstY) / RowPitch; + const int ListBottomY = RowFirstY + ListRows * RowPitch - 6; const int ScrollUpY = HeaderDividerY + 90; - const int ScrollDownY = FooterDividerY - 104; + const int ScrollDownY = ListBottomY - (ScrollUpY - RowFirstY) - ScrollArrowH; const char * const AssetBadgeOuter = "/images/ui/badge_outer.png"; const char * const AssetBadgeInner = "/images/ui/badge_inner.png"; diff --git a/src/main.cpp b/src/main.cpp index c134af1..94a46a1 100644 --- a/src/main.cpp +++ b/src/main.cpp @@ -13,6 +13,7 @@ #include "framework/controller.h" #include "framework/powerwatch.h" #include "framework/draw_helpers.h" +#include "framework/list_screen.h" #include "framework/utf8.h" #include "application_lifecycle.h" #include "back_stack.h" @@ -197,11 +198,7 @@ int main(int argc, char * argv[]) Texture titleText(titleString, UiTheme::SectionTitleFontSize, renderer, UiTheme::SectionTitleX, UiTheme::SectionTitleY, false, ToAbgr(UiTheme::Text), true); SDL_Rect selectedRowRect{ UiTheme::ListX, UiTheme::RowFirstY - 2, UiTheme::ListContentRightX - UiTheme::ListX, 0 }; Texture CompComText("created by CompCom - Modern UI by DefKorns", 16, renderer, UiTheme::CreditX, UiTheme::CreditY, false, ToAbgr(UiTheme::Text), true); - Texture scrollUp(optionsLocation + UiTheme::AssetChevronUp, renderer, UiTheme::ScrollX, UiTheme::ScrollUpY); - SetColorMod(scrollUp.texture.get(), UiTheme::ScrollArrow); - scrollUp.rect.x -= scrollUp.rect.w / 2; - Texture scrollDown = scrollUp; - scrollDown.rect.y = UiTheme::ScrollDownY; + ScrollArrows scrollArrows(optionsLocation, renderer); BadgePainter badges(optionsLocation, renderer); auto MakeBadge = [&](const char * letter, const char * hintKey, Color rim, Color fill) -> Badge @@ -444,10 +441,7 @@ int main(int argc, char * argv[]) CompComText.Draw(renderer); - if(topListItemNumber != 0) - scrollUp.Draw(renderer); - if((topListItemNumber + DisplayItemCount) < commandCount) - scrollDown.Draw(renderer, SDL_FLIP_VERTICAL); + scrollArrows.Draw(topListItemNumber != 0, (topListItemNumber + DisplayItemCount) < commandCount); }; auto Confirm = [&](const std::string & title) -> bool diff --git a/src/mod_uninstall.cpp b/src/mod_uninstall.cpp index e204b5b..b8c52b9 100644 --- a/src/mod_uninstall.cpp +++ b/src/mod_uninstall.cpp @@ -112,10 +112,7 @@ int main(int argc, char * argv[]) mod.text = Texture(TruncateToWidth(mod.name, ListFontSize, RowTextMaxW), ListFontSize, renderer, UiTheme::RowTextX, 0, false, ToAbgr(White), true); Texture emptyText(Translate("MOD_UNINSTALL_EMPTY"), ListFontSize, renderer, UiTheme::RowTextX, UiTheme::RowFirstY, false, ToAbgr(UiTheme::TextDim), true); - Texture scrollUp(OptionsLocation + UiTheme::AssetChevronUp, renderer, UiTheme::ScrollX, UiTheme::ScrollUpY); - SetColorMod(scrollUp.texture.get(), UiTheme::ScrollArrow); - Texture scrollDown = scrollUp; - scrollDown.rect.y = UiTheme::ScrollDownY; + ScrollArrows scrollArrows(OptionsLocation, renderer); const int PanelPad = 16; const int PanelInnerX = UiTheme::DetailX + PanelPad; @@ -209,10 +206,7 @@ int main(int argc, char * argv[]) checkbox.rect.y = centerY - checkbox.rect.h/2; checkbox.Draw(renderer); } - if(listOffset != 0) - scrollUp.Draw(renderer); - if(listOffset + displayCount < modCount) - scrollDown.Draw(renderer, SDL_FLIP_VERTICAL); + scrollArrows.Draw(listOffset != 0, listOffset + displayCount < modCount); }; auto DrawSummary = [&]() diff --git a/src/save_manager.cpp b/src/save_manager.cpp index 736f7fe..f9b45b8 100644 --- a/src/save_manager.cpp +++ b/src/save_manager.cpp @@ -183,10 +183,7 @@ int main(int, char **) const int RowTextMaxW = CheckboxX - 16 - UiTheme::RowTextX; const int displayCount = std::max(1, (UiTheme::FooterDividerY - UiTheme::ListBottomMargin - UiTheme::RowFirstY) / UiTheme::RowPitch); - Texture scrollUp(OptionsLocation + UiTheme::AssetChevronUp, renderer, UiTheme::ScrollX, UiTheme::ScrollUpY); - SetColorMod(scrollUp.texture.get(), UiTheme::ScrollArrow); - Texture scrollDown = scrollUp; - scrollDown.rect.y = UiTheme::ScrollDownY; + ScrollArrows scrollArrows(OptionsLocation, renderer); const int PanelPad = 16; const int PanelInnerX = DetailX + PanelPad; @@ -342,10 +339,7 @@ int main(int, char **) checkbox.rect.y = centerY - checkbox.rect.h/2; checkbox.Draw(renderer); } - if(listOffset != 0) - scrollUp.Draw(renderer); - if(listOffset + displayCount < rowCount) - scrollDown.Draw(renderer, SDL_FLIP_VERTICAL); + scrollArrows.Draw(listOffset != 0, listOffset + displayCount < rowCount); DrawStrokeRect(renderer, summaryBox, UiTheme::Border, UiTheme::BorderWidth, UiTheme::BoxRadius); summaryHeader.Draw(renderer); From 093d7e28f89a926838b052131604de2d6fed1187 Mon Sep 17 00:00:00 2001 From: DefKorns Date: Wed, 21 Oct 2026 09:41:40 +0000 Subject: [PATCH 4/7] Confirm non-destructive actions with A and keep Start+Select for destructive ones --- src/framework/panel_dialog.cpp | 14 ++++++-------- src/framework/panel_dialog.h | 4 ++-- src/mod_uninstall.cpp | 2 +- src/save_manager.cpp | 18 +++++++++++------- 4 files changed, 20 insertions(+), 18 deletions(-) diff --git a/src/framework/panel_dialog.cpp b/src/framework/panel_dialog.cpp index cfc0432..91516bb 100644 --- a/src/framework/panel_dialog.cpp +++ b/src/framework/panel_dialog.cpp @@ -147,14 +147,12 @@ DialogResult PanelDialog::Run(SDL_Context & sdl, Controller & controller, const controller.Update(); if(quit || sdl.powerwatch->buttonPress()) return Finish(controller, DialogResult::Quit); - if(kind_ == Kind::Confirm) - { - if(controller.GetButtonStatus(B)) - return Finish(controller, DialogResult::Cancelled); - if(controller.PeekButtonStatus(START) && controller.PeekButtonStatus(SELECT)) - return Finish(controller, DialogResult::Confirmed); - } - else if(controller.GetButtonStatus(A)) + if(kind_ != Kind::Acknowledge && controller.GetButtonStatus(B)) + return Finish(controller, DialogResult::Cancelled); + const bool confirmed = kind_ == Kind::ConfirmDestructive + ? controller.PeekButtonStatus(START) && controller.PeekButtonStatus(SELECT) + : controller.GetButtonStatus(A); + if(confirmed) return Finish(controller, DialogResult::Confirmed); sdl.StartFrame(); diff --git a/src/framework/panel_dialog.h b/src/framework/panel_dialog.h index f566997..ee55b18 100644 --- a/src/framework/panel_dialog.h +++ b/src/framework/panel_dialog.h @@ -39,11 +39,11 @@ struct PanelDialogText std::string note; }; -// Acknowledge closes on A only; Confirm needs Start + Select held, B cancels. +// Acknowledge closes on A only. Confirm takes A, ConfirmDestructive needs Start + Select held; both cancel on B. class PanelDialog { public: - enum class Kind { Acknowledge, Confirm }; + enum class Kind { Acknowledge, Confirm, ConfirmDestructive }; PanelDialog(SDL_Renderer * renderer, BadgePainter & painter, Kind kind, const PanelDialogText & text, std::vector badges); void SetBulletColor(Color color); diff --git a/src/mod_uninstall.cpp b/src/mod_uninstall.cpp index b8c52b9..9c8e1d4 100644 --- a/src/mod_uninstall.cpp +++ b/src/mod_uninstall.cpp @@ -281,7 +281,7 @@ int main(int argc, char * argv[]) text.body.push_back(marked[i]->name); text.hiddenCount = std::max(0, static_cast(marked.size()) - maxNames); text.note = Translate("MOD_UNINSTALL_REBOOT_NOTE"); - PanelDialog dialog(renderer, badges, PanelDialog::Kind::Confirm, text, { &badgeCancel, &confirmPill }); + PanelDialog dialog(renderer, badges, PanelDialog::Kind::ConfirmDestructive, text, { &badgeCancel, &confirmPill }); dialog.SetBulletColor(UiTheme::CheckboxOn); const DialogResult answer = dialog.Run(sdl_context, controller, DrawScreen); if(answer != DialogResult::Confirmed) diff --git a/src/save_manager.cpp b/src/save_manager.cpp index f9b45b8..fe42d1a 100644 --- a/src/save_manager.cpp +++ b/src/save_manager.cpp @@ -65,11 +65,12 @@ namespace std::string listArgs; std::string actionArgs; bool preMarked; + bool destructive; }; - const ListInfo CleanList{ "SAVE_MANAGER_CLEAN", "SAVE_MANAGER_TO_MOVE", "SAVE_MANAGER_CLEAN_NOTE", "SAVE_MANAGER_CLEAN_CONFIRM", "HINT_MOVE", "orphans", "move ", true }; - const ListInfo RestoreDeletedList{ "SAVE_MANAGER_RESTORE_DELETED", "SAVE_MANAGER_TO_RESTORE", "SAVE_MANAGER_RESTORE_NOTE", "SAVE_MANAGER_RESTORE_DELETED_CONFIRM", "HINT_RESTORE", "deleted", "restore ", false }; - const ListInfo RestoreBackupList{ "RESTORE_SAVES_USB", "SAVE_MANAGER_TO_RESTORE", "SAVE_MANAGER_RESTORE_USB_NOTE", "SAVE_MANAGER_RESTORE_CONFIRM", "HINT_RESTORE", "backup", "restore-backup ", false }; + const ListInfo CleanList{ "SAVE_MANAGER_CLEAN", "SAVE_MANAGER_TO_MOVE", "SAVE_MANAGER_CLEAN_NOTE", "SAVE_MANAGER_CLEAN_CONFIRM", "HINT_MOVE", "orphans", "move ", true, true }; + const ListInfo RestoreDeletedList{ "SAVE_MANAGER_RESTORE_DELETED", "SAVE_MANAGER_TO_RESTORE", "SAVE_MANAGER_RESTORE_NOTE", "SAVE_MANAGER_RESTORE_DELETED_CONFIRM", "HINT_RESTORE", "deleted", "restore ", false, false }; + const ListInfo RestoreBackupList{ "RESTORE_SAVES_USB", "SAVE_MANAGER_TO_RESTORE", "SAVE_MANAGER_RESTORE_USB_NOTE", "SAVE_MANAGER_RESTORE_CONFIRM", "HINT_RESTORE", "backup", "restore-backup ", false, true }; struct ActionInfo { @@ -212,14 +213,17 @@ int main(int, char **) Badge badgeOk = MakeBadge("A", "HINT_OK", UiTheme::BadgeADark, UiTheme::BadgeA); Badge badgeBack = MakeBadge("B", "HINT_BACK", UiTheme::BadgeBDark, UiTheme::BadgeB); Badge badgeCancel = MakeBadge("B", "HINT_CANCEL", UiTheme::BadgeBDark, UiTheme::BadgeB); + Badge badgeConfirm = MakeBadge("A", "HINT_CONFIRM", UiTheme::BadgeADark, UiTheme::BadgeA); Badge confirmPill = badges.MakePill("Start + Select", Translate("HINT_CONFIRM")); auto DrawBadge = [&](Badge & badge, int rightEdgeX) -> int { return badges.Draw(badge, rightEdgeX, UiTheme::BadgeBandY); }; - auto Confirm = [&](const std::function & drawUnder, const PanelDialogText & text) + auto Confirm = [&](const std::function & drawUnder, const PanelDialogText & text, bool destructive) { - return PanelDialog(renderer, badges, PanelDialog::Kind::Confirm, text, { &badgeCancel, &confirmPill }).Run(sdl_context, controller, drawUnder); + if(destructive) + return PanelDialog(renderer, badges, PanelDialog::Kind::ConfirmDestructive, text, { &badgeCancel, &confirmPill }).Run(sdl_context, controller, drawUnder); + return PanelDialog(renderer, badges, PanelDialog::Kind::Confirm, text, { &badgeCancel, &badgeConfirm }).Run(sdl_context, controller, drawUnder); }; auto ShowMessage = [&](const std::function & drawUnder, const std::string & titleKey) @@ -453,7 +457,7 @@ int main(int, char **) text.body.push_back(marked[i]->label); text.hiddenCount = std::max(0, static_cast(marked.size()) - maxNames); text.note = Translate(list.noteKey); - const DialogResult answer = Confirm(DrawScreen, text); + const DialogResult answer = Confirm(DrawScreen, text, list.destructive); if(answer == DialogResult::Quit) return ExitLeaveMenu; if(answer == DialogResult::Confirmed) @@ -600,7 +604,7 @@ int main(int, char **) PanelDialogText text; text.title = Translate("SAVE_MANAGER_BACKUP_CONFIRM"); text.note = Translate("SAVE_MANAGER_BACKUP_DESC"); - const DialogResult answer = Confirm(DrawActions, text); + const DialogResult answer = Confirm(DrawActions, text, true); if(answer == DialogResult::Quit) return ExitLeaveMenu; if(answer == DialogResult::Confirmed) From 09181cece4d5c40abaa76c67c9a3f65802104e3c Mon Sep 17 00:00:00 2001 From: DefKorns Date: Wed, 21 Oct 2026 09:42:31 +0000 Subject: [PATCH 5/7] Tag repeated save names with their code and hide empty saves --- mod/etc/options_menu/scripts/OrphanSaves.sh | 3 +- src/save_manager.cpp | 36 +++++++++++++++++++-- 2 files changed, 35 insertions(+), 4 deletions(-) diff --git a/mod/etc/options_menu/scripts/OrphanSaves.sh b/mod/etc/options_menu/scripts/OrphanSaves.sh index 47a9c93..7c443e9 100644 --- a/mod/etc/options_menu/scripts/OrphanSaves.sh +++ b/mod/etc/options_menu/scripts/OrphanSaves.sh @@ -42,8 +42,9 @@ raContent() { sed -E "s/$ra_save_suffix//" } +# a game folder with only empty files holds no save saveEntries() { - ls "$1" 2>/dev/null | grep "^CLV-" | grep -v "^CLV-S-" + find "$1" -mindepth 2 -path "$1/CLV-*" -type f -size +0 2>/dev/null | sed "s#^$1/##; s#/.*##" | grep -v "^CLV-S-" | sort -u ls "$1/retroarch" 2>/dev/null | raContent | sed 's#^#retroarch/#' } diff --git a/src/save_manager.cpp b/src/save_manager.cpp index fe42d1a..505536c 100644 --- a/src/save_manager.cpp +++ b/src/save_manager.cpp @@ -45,6 +45,8 @@ namespace const int DetailW = UiTheme::FrameX + UiTheme::FrameW - 16 - DetailX; const int ListFontSize = 16; + const int TagFontSize = 14; + const int TagGap = 12; const int PanelFontSize = 16; const int PanelHeaderFontSize = 18; @@ -93,7 +95,9 @@ namespace std::string code; std::string label; bool named = false; + std::string tag; Texture text; + Texture tagText; bool marked = false; }; @@ -149,6 +153,16 @@ namespace return retroArch ? "RetroArch: " + shown : shown; } + std::string EntryTag(const std::string & entry) + { + return entry.compare(0, RetroArchPrefix.size(), RetroArchPrefix) == 0 ? entry.substr(RetroArchPrefix.size()) : entry; + } + + std::string ShownLabel(const SaveEntry & entry) + { + return entry.tag.empty() ? entry.label : entry.label + " (" + entry.tag + ")"; + } + void DrawDot(SDL_Renderer * renderer, int x, int centerY) { DrawRoundedFillRect(renderer, { x, centerY - 4, 8, 8 }, UiTheme::Accent, 4); @@ -283,7 +297,7 @@ int main(int, char **) int shown = (count > SummaryMaxRows) ? SummaryMaxRows - 1 : count; summaryNames.clear(); for(int i = 0; i < shown; ++i) - summaryNames.emplace_back(TruncateToWidth(marked[i]->label, PanelFontSize, PanelInnerW - BulletTextGap), PanelFontSize, renderer, PanelInnerX + BulletTextGap, 0, false, ToAbgr(UiTheme::Text), true); + summaryNames.emplace_back(TruncateToWidth(ShownLabel(*marked[i]), PanelFontSize, PanelInnerW - BulletTextGap), PanelFontSize, renderer, PanelInnerX + BulletTextGap, 0, false, ToAbgr(UiTheme::Text), true); moreText = (shown < count) ? Texture("+" + std::to_string(count - shown), PanelFontSize, renderer, PanelInnerX + BulletTextGap, 0, false, ToAbgr(UiTheme::TextDim), true) : Texture(); @@ -292,11 +306,20 @@ int main(int, char **) // named entries first, alphabetically; the rest keep their code order auto ApplyNames = [&]() { + std::map labelCount; for(SaveEntry & entry : entries) { entry.named = gameNames.count(entry.code) != 0; entry.label = EntryLabel(entry.code, gameNames); - entry.text = Texture(TruncateToWidth(entry.label, ListFontSize, RowTextMaxW), ListFontSize, renderer, UiTheme::RowTextX, 0, false, ToAbgr(White), true); + ++labelCount[entry.label]; + } + for(SaveEntry & entry : entries) + { + const bool repeated = labelCount[entry.label] > 1; + entry.tag = repeated ? EntryTag(entry.code) : ""; + entry.tagText = repeated ? Texture(entry.tag, TagFontSize, renderer, 0, 0, false, ToAbgr(UiTheme::TextDim), true) : Texture(); + const int textMaxW = repeated ? RowTextMaxW - entry.tagText.rect.w - TagGap : RowTextMaxW; + entry.text = Texture(TruncateToWidth(entry.label, ListFontSize, textMaxW), ListFontSize, renderer, UiTheme::RowTextX, 0, false, ToAbgr(White), true); } std::stable_sort(entries.begin(), entries.end(), [](const SaveEntry & a, const SaveEntry & b) { @@ -338,6 +361,13 @@ int main(int, char **) text.rect.y = centerY - text.rect.h/2; SetColorMod(text.texture.get(), row != 0 && marked ? UiTheme::TextDim : UiTheme::Text); text.Draw(renderer); + if(row != 0 && entries[row - selectAllRows].tagText.texture) + { + Texture & tag = entries[row - selectAllRows].tagText; + tag.rect.x = CheckboxX - TagGap - tag.rect.w; + tag.rect.y = centerY - tag.rect.h/2; + tag.Draw(renderer); + } Texture & checkbox = marked ? checkboxOn : checkboxOff; checkbox.rect.x = CheckboxX; checkbox.rect.y = centerY - checkbox.rect.h/2; @@ -454,7 +484,7 @@ int main(int, char **) PanelDialogText text; text.title = Translate(list.confirmKey); for(int i = 0; i < std::min(maxNames, static_cast(marked.size())); ++i) - text.body.push_back(marked[i]->label); + text.body.push_back(ShownLabel(*marked[i])); text.hiddenCount = std::max(0, static_cast(marked.size()) - maxNames); text.note = Translate(list.noteKey); const DialogResult answer = Confirm(DrawScreen, text, list.destructive); From 8b644b67a05c8552d1245bf4f1f6068401f6368f Mon Sep 17 00:00:00 2001 From: DefKorns Date: Wed, 21 Oct 2026 10:02:49 +0000 Subject: [PATCH 6/7] Skip the section accent bar when a screen has no title --- src/framework/list_screen.cpp | 9 ++++++--- 1 file changed, 6 insertions(+), 3 deletions(-) diff --git a/src/framework/list_screen.cpp b/src/framework/list_screen.cpp index 15b6a9b..9ed1dfd 100644 --- a/src/framework/list_screen.cpp +++ b/src/framework/list_screen.cpp @@ -62,9 +62,12 @@ void ScreenChrome::Draw() appVersion_.Draw(renderer_); DrawHLine(renderer_, UiTheme::HeaderDividerX, UiTheme::HeaderDividerX + UiTheme::HeaderDividerW, UiTheme::HeaderDividerY, UiTheme::Border, UiTheme::BorderWidth); DrawHLine(renderer_, UiTheme::OuterRect.x, UiTheme::OuterRect.x + UiTheme::OuterRect.w, UiTheme::FooterDividerY, UiTheme::Border, UiTheme::BorderWidth); - const int accentBarY = sectionTitle_.rect.y + (sectionTitle_.rect.h - UiTheme::SectionAccentBarH) / 2; - DrawFillRect(renderer_, { UiTheme::SectionTitleX - UiTheme::SectionAccentBarW - SectionAccentGap, accentBarY, UiTheme::SectionAccentBarW, UiTheme::SectionAccentBarH }, UiTheme::Accent); - sectionTitle_.Draw(renderer_); + if(sectionTitle_.texture) + { + const int accentBarY = sectionTitle_.rect.y + (sectionTitle_.rect.h - UiTheme::SectionAccentBarH) / 2; + DrawFillRect(renderer_, { UiTheme::SectionTitleX - UiTheme::SectionAccentBarW - SectionAccentGap, accentBarY, UiTheme::SectionAccentBarW, UiTheme::SectionAccentBarH }, UiTheme::Accent); + sectionTitle_.Draw(renderer_); + } credit_.Draw(renderer_); } From cd61c3414398ad23e8dd12842766c86639e2cb18 Mon Sep 17 00:00:00 2001 From: DefKorns Date: Wed, 21 Oct 2026 10:08:36 +0000 Subject: [PATCH 7/7] Cap NoteBox text to the lines that fit its box --- src/framework/list_screen.cpp | 16 +++++++++++++++- 1 file changed, 15 insertions(+), 1 deletion(-) diff --git a/src/framework/list_screen.cpp b/src/framework/list_screen.cpp index 9ed1dfd..116af17 100644 --- a/src/framework/list_screen.cpp +++ b/src/framework/list_screen.cpp @@ -19,6 +19,9 @@ #include "draw_helpers.h" #include "text_layout.h" #include "uitheme.h" +#include "utf8.h" + +#include namespace { @@ -117,7 +120,18 @@ NoteBox::NoteBox(SDL_Renderer * renderer, SDL_Rect rect) void NoteBox::SetText(const std::string & text) { const int textW = rect_.w - NotePadRight - NoteTextOffsetX; - lines_ = MakeLines(WrapToWidth(text, NoteFontSize, textW), NoteFontSize, renderer_, UiTheme::TextDim); + std::vector wrapped = WrapToWidth(text, NoteFontSize, textW); + const size_t maxLines = static_cast(std::max(1, rect_.h / NoteLinePitch)); + if(wrapped.size() > maxLines) + { + wrapped.resize(maxLines); + std::string & last = wrapped.back(); + int n = Utf8Length(last); + while(n > 0 && TextWidth(TruncateUtf8(last, n) + "...", NoteFontSize) > textW) + --n; + last = TruncateUtf8(last, n) + "..."; + } + lines_ = MakeLines(wrapped, NoteFontSize, renderer_, UiTheme::TextDim); } void NoteBox::Draw()