diff --git a/Makefile b/Makefile
index 34b686c..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
+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/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/framework/list_screen.cpp b/src/framework/list_screen.cpp
new file mode 100644
index 0000000..116af17
--- /dev/null
+++ b/src/framework/list_screen.cpp
@@ -0,0 +1,153 @@
+/**
+ * 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"
+#include "utf8.h"
+
+#include
+
+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);
+ 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_);
+}
+
+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;
+}
+
+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)
+{
+}
+
+void NoteBox::SetText(const std::string & text)
+{
+ const int textW = rect_.w - NotePadRight - NoteTextOffsetX;
+ 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()
+{
+ 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..1debea3
--- /dev/null
+++ b/src/framework/list_screen.h
@@ -0,0 +1,73 @@
+/**
+ * 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 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:
+ 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/panel_dialog.cpp b/src/framework/panel_dialog.cpp
new file mode 100644
index 0000000..91516bb
--- /dev/null
+++ b/src/framework/panel_dialog.cpp
@@ -0,0 +1,185 @@
+/**
+ * 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::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();
+ 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..ee55b18
--- /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 takes A, ConfirmDestructive needs Start + Select held; both cancel on B.
+class PanelDialog
+{
+public:
+ enum class Kind { Acknowledge, Confirm, ConfirmDestructive };
+
+ 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/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/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 dca49ad..9c8e1d4 100644
--- a/src/mod_uninstall.cpp
+++ b/src/mod_uninstall.cpp
@@ -8,12 +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"
@@ -31,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;
@@ -95,13 +95,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);
@@ -117,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;
@@ -131,15 +123,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 +148,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 +182,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 +194,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);
@@ -243,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 = [&]()
@@ -285,25 +245,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 +260,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 = [&]()
@@ -324,90 +271,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::ConfirmDestructive, 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;
@@ -454,7 +341,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 3f9bdab..505536c 100644
--- a/src/save_manager.cpp
+++ b/src/save_manager.cpp
@@ -9,12 +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"
@@ -44,9 +45,10 @@ 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;
- const int NoteFontSize = 15;
const int ExitBack = 0;
const int ExitLeaveMenu = 1;
@@ -65,11 +67,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
{
@@ -92,7 +95,9 @@ namespace
std::string code;
std::string label;
bool named = false;
+ std::string tag;
Texture text;
+ Texture tagText;
bool marked = false;
};
@@ -148,20 +153,19 @@ namespace
return retroArch ? "RetroArch: " + shown : shown;
}
- void DrawDot(SDL_Renderer * renderer, int x, int centerY)
+ std::string EntryTag(const std::string & entry)
{
- DrawRoundedFillRect(renderer, { x, centerY - 4, 8, 8 }, UiTheme::Accent, 4);
+ return entry.compare(0, RetroArchPrefix.size(), RetroArchPrefix) == 0 ? entry.substr(RetroArchPrefix.size()) : entry;
}
- void DrawLinesCentered(SDL_Renderer * renderer, std::vector & lines, int centerX, int y, int pitch)
+ std::string ShownLabel(const SaveEntry & entry)
{
- for(Texture & line : lines)
- {
- line.rect.x = centerX - line.rect.w/2;
- line.rect.y = y;
- line.Draw(renderer);
- y += pitch;
- }
+ 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);
}
}
@@ -178,16 +182,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;
@@ -200,10 +198,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;
@@ -214,14 +209,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
@@ -234,161 +227,31 @@ 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 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)
+ auto Confirm = [&](const std::function & drawUnder, const PanelDialogText & text, bool destructive)
{
- 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;
- }
+ 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 NoteLines = [&](const std::string & key)
+ auto ShowMessage = [&](const std::function & drawUnder, const std::string & titleKey)
{
- 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
- {
- 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();
- }
- };
-
- // Shows a progress panel for one frame; the caller then runs the blocking work.
- auto ShowBusy = [&](const std::function & drawUnder, const std::string & messageKey)
- {
- 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;
};
@@ -398,7 +261,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();
@@ -434,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();
@@ -443,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)
{
@@ -475,29 +347,33 @@ 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;
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;
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);
@@ -532,7 +408,7 @@ int main(int, char **)
moreText.Draw(renderer);
}
}
- DrawNote(noteLines);
+ note.Draw();
int rightEdge = UiTheme::BadgeClusterRightX;
const int row = listOffset + currentId;
@@ -541,7 +417,7 @@ int main(int, char **)
rightEdge = DrawBadge(badgeBack, rightEdge);
if(markedCount > 0)
rightEdge = DrawBadge(actionPill, rightEdge);
- DrawFooterDivider(rightEdge);
+ DrawFooterDivider(renderer, rightEdge);
};
auto Labels = [&]()
@@ -604,13 +480,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(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);
+ if(answer == DialogResult::Quit)
+ return ExitLeaveMenu;
+ if(answer == DialogResult::Confirmed)
{
const bool fromBackup = &list == &RestoreBackupList;
bool ok;
@@ -627,8 +507,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;
@@ -699,10 +579,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,12 +622,7 @@ int main(int, char **)
int rightEdge = UiTheme::BadgeClusterRightX;
rightEdge = DrawBadge(badgeSelect, rightEdge);
rightEdge = DrawBadge(badgeBack, rightEdge);
- DrawFooterDivider(rightEdge);
- };
-
- auto ShowMessage = [&](const std::string & titleKey, const std::string & noteKey)
- {
- RunDialog(DrawActions, titleKey, {}, noteKey, false);
+ DrawFooterDivider(renderer, rightEdge);
};
auto RunAction = [&](Action action) -> int
@@ -755,13 +630,22 @@ int main(int, char **)
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, true);
+ 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:
@@ -769,8 +653,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");