From ea9c04bccb35cacb13460e35b7e0252395dfa931 Mon Sep 17 00:00:00 2001 From: Collecting Date: Wed, 28 Jan 2026 07:38:38 +0100 Subject: [PATCH] feat(ui): Surprise Me! Logic The main logic for the Surprise Me feature. Signed-off-by: Collecting --- src/citron/game_list.cpp | 292 +++++++++++++++++++++++++++++++++++++++ 1 file changed, 292 insertions(+) diff --git a/src/citron/game_list.cpp b/src/citron/game_list.cpp index 57d0a7a1e..4739e0f03 100644 --- a/src/citron/game_list.cpp +++ b/src/citron/game_list.cpp @@ -29,6 +29,15 @@ #include #include #include +#include +#include +#include +#include +#include +#include +#include +#include +#include #include #include "common/common_types.h" #include "common/logging/log.h" @@ -47,6 +56,211 @@ #include "citron/uisettings.h" #include "citron/util/controller_navigation.h" +// A helper struct to cleanly pass game data +struct SurpriseGame { + QString name; + QString path; + quint64 title_id; + QPixmap icon; +}; + +// This is the custom widget that shows the actual spinning game icons +class GameReelWidget : public QWidget { + Q_OBJECT + Q_PROPERTY(qreal scrollOffset READ getScrollOffset WRITE setScrollOffset) + +public: + explicit GameReelWidget(QWidget* parent = nullptr) : QWidget(parent), m_scroll_offset(0.0) { + setFixedHeight(150); + } + + void setGameReel(const QVector& games) { + m_games = games; + update(); + } + + qreal getScrollOffset() const { return m_scroll_offset; } + void setScrollOffset(qreal offset) { + m_scroll_offset = offset; + update(); + } + +protected: + void paintEvent(QPaintEvent* event) override { + if (m_games.isEmpty()) return; + QPainter painter(this); + painter.setRenderHint(QPainter::Antialiasing); + + const int icon_size = 128; + const int icon_spacing = 15; + const int total_slot_width = icon_size + icon_spacing; + + const int widget_center_x = width() / 2; + const int widget_center_y = height() / 2; + + painter.fillRect(rect(), palette().color(QPalette::Window)); + QColor highlight_color = palette().color(QPalette::Highlight); + painter.fillRect(widget_center_x - 2, 0, 4, height(), highlight_color); + + for (int i = 0; i < m_games.size(); ++i) { + const qreal icon_x_position = (widget_center_x - icon_size / 2) + (i * total_slot_width) - m_scroll_offset; + const int draw_x = static_cast(icon_x_position); + const int draw_y = widget_center_y - (icon_size / 2); + + if (draw_x + icon_size < 0 || draw_x > width()) { + continue; + } + + painter.save(); + + QPainterPath path; + path.addRoundedRect(draw_x, draw_y, icon_size, icon_size, 12, 12); + painter.setClipPath(path); + + painter.drawPixmap(draw_x, draw_y, icon_size, icon_size, m_games[i].icon); + + painter.restore(); + } + } + +private: + QVector m_games; + qreal m_scroll_offset; +}; + +// This is the main pop-up window that holds the spinning icons, title, and buttons +class SurpriseMeDialog : public QDialog { + Q_OBJECT + +public: + explicit SurpriseMeDialog(QVector games, QWidget* parent = nullptr) + : QDialog(parent), m_available_games(games), m_last_choice({QString(), QString(), 0, QPixmap()}) { + setWindowTitle(tr("Surprise Me!")); + setModal(true); + setFixedSize(540, 280); + + auto* layout = new QVBoxLayout(this); + layout->setSpacing(15); + layout->setContentsMargins(15, 15, 15, 15); + + m_reel_widget = new GameReelWidget(this); + m_game_title_label = new QLabel(tr("Spinning..."), this); + m_launch_button = new QPushButton(tr("Launch Game"), this); + m_reroll_button = new QPushButton(tr("Try Again?"), this); + + m_launch_button->setSizePolicy(QSizePolicy::Expanding, QSizePolicy::Fixed); + m_reroll_button->setSizePolicy(QSizePolicy::Expanding, QSizePolicy::Fixed); + + m_launch_button->setStyleSheet(QStringLiteral("padding: 5px;")); + m_reroll_button->setStyleSheet(QStringLiteral("padding: 5px;")); + m_launch_button->setMinimumHeight(35); + m_reroll_button->setMinimumHeight(35); + + QFont title_font = m_game_title_label->font(); + title_font.setPointSize(16); + title_font.setBold(true); + m_game_title_label->setFont(title_font); + m_game_title_label->setAlignment(Qt::AlignCenter); + + m_game_title_label->setWordWrap(true); + + auto* button_layout = new QHBoxLayout(); + button_layout->addWidget(m_reroll_button); + button_layout->addWidget(m_launch_button); + + layout->addWidget(m_reel_widget); + layout->addWidget(m_game_title_label); + layout->addLayout(button_layout); + + m_launch_button->setEnabled(false); + m_reroll_button->setEnabled(false); + + m_animation = new QPropertyAnimation(m_reel_widget, "scrollOffset", this); + m_animation->setEasingCurve(QEasingCurve::OutCubic); + + connect(m_launch_button, &QPushButton::clicked, this, &SurpriseMeDialog::onLaunch); + connect(m_reroll_button, &QPushButton::clicked, this, &SurpriseMeDialog::startRoll); + + QTimer::singleShot(100, this, &SurpriseMeDialog::startRoll); + } + + const SurpriseGame& getFinalChoice() const { return m_last_choice; } + +private slots: + void startRoll() { + if (m_available_games.isEmpty()) { + m_game_title_label->setText(tr("No more games to choose!")); + m_reroll_button->setEnabled(false); + return; + } + + m_game_title_label->setText(tr("Spinning...")); + m_launch_button->setEnabled(false); + m_reroll_button->setEnabled(false); + + std::random_device rd; + std::mt19937 gen(rd()); + std::uniform_int_distribution<> full_distrib(0, m_available_games.size() - 1); + const int winning_index = full_distrib(gen); + + const SurpriseGame winner = m_available_games.at(winning_index); + m_available_games.removeAt(winning_index); + + QVector reel; + if (!m_available_games.isEmpty()) { + std::uniform_int_distribution<> filler_distrib(0, m_available_games.size() - 1); + for (int i = 0; i < 20; ++i) reel.push_back(m_available_games.at(filler_distrib(gen))); + reel.push_back(winner); + for (int i = 0; i < 20; ++i) reel.push_back(m_available_games.at(filler_distrib(gen))); + } else { + reel.push_back(winner); + } + + m_reel_widget->setGameReel(reel); + + const int icon_size = 128; + const int icon_spacing = 15; + const int total_slot_width = icon_size + icon_spacing; + const qreal start_offset = 0; + + const int winning_reel_index = m_available_games.isEmpty() ? 0 : 20; + const qreal end_offset = (winning_reel_index * total_slot_width); + + m_animation->stop(); + m_reel_widget->setScrollOffset(start_offset); + m_animation->setDuration(4000); + m_animation->setStartValue(start_offset); + m_animation->setEndValue(end_offset); + + disconnect(m_animation, &QPropertyAnimation::finished, nullptr, nullptr); + connect(m_animation, &QPropertyAnimation::finished, this, [this, winner]() { + m_last_choice = winner; + onRollFinished(); + }); + + m_animation->start(); + } + + void onRollFinished() { + m_game_title_label->setText(m_last_choice.name); + m_launch_button->setEnabled(true); + if (!m_available_games.isEmpty()) { + m_reroll_button->setEnabled(true); + } + } + + void onLaunch() { accept(); } + +private: + QVector m_available_games; + SurpriseGame m_last_choice; + GameReelWidget* m_reel_widget; + QLabel* m_game_title_label; + QPushButton* m_launch_button; + QPushButton* m_reroll_button; + QPropertyAnimation* m_animation; +}; + // Static helper for Save Detection static QString GetDetectedEmulatorName(const QString& path, u64 program_id, const QString& citron_nand_base) { QString abs_path = QDir(path).absolutePath(); @@ -698,6 +912,25 @@ play_time_manager{play_time_manager_}, system{system_} { )); connect(btn_sort_az, &QToolButton::clicked, this, &GameList::ToggleSortOrder); + // Surprise Me button - positioned after sort button + btn_surprise_me = new QToolButton(toolbar); + btn_surprise_me->setIcon(QIcon(QStringLiteral(":/dist/dice.svg"))); + btn_surprise_me->setToolTip(tr("Surprise Me! (Choose Random Game)")); + btn_surprise_me->setAutoRaise(true); + btn_surprise_me->setIconSize(QSize(16, 16)); + btn_surprise_me->setFixedSize(32, 32); + btn_surprise_me->setStyleSheet(QStringLiteral( + "QToolButton {" + " border: 1px solid palette(mid);" + " border-radius: 4px;" + " background: palette(button);" + "}" + "QToolButton:hover {" + " background: palette(light);" + "}" + )); + connect(btn_surprise_me, &QToolButton::clicked, this, &GameList::onSurpriseMeClicked); + // Create progress bar progress_bar = new QProgressBar(this); progress_bar->setVisible(false); @@ -713,6 +946,7 @@ play_time_manager{play_time_manager_}, system{system_} { toolbar_layout->addWidget(btn_grid_view); toolbar_layout->addWidget(slider_title_size); toolbar_layout->addWidget(btn_sort_az); + toolbar_layout->addWidget(btn_surprise_me); toolbar_layout->addStretch(); // Push search to the right toolbar_layout->addWidget(search_field); @@ -1884,3 +2118,61 @@ void GameList::RefreshCompatibilityList() { reply->deleteLater(); }); } + +void GameList::onSurpriseMeClicked() { + QVector all_games; + + // Go through the list and gather info for every game (name, icon, path) + for (int i = 0; i < item_model->rowCount(); ++i) { + QStandardItem* folder = item_model->item(i, 0); + if (!folder || folder->data(GameListItem::TypeRole).value() == GameListItemType::AddDir) { + continue; + } + + for (int j = 0; j < folder->rowCount(); ++j) { + QStandardItem* game_item = folder->child(j, 0); + if (game_item && game_item->data(GameListItem::TypeRole).value() == GameListItemType::Game) { + QString game_title = game_item->data(GameListItemPath::TitleRole).toString(); + if (game_title.isEmpty()) { + std::string filename; + Common::SplitPath(game_item->data(GameListItemPath::FullPathRole).toString().toStdString(), nullptr, &filename, nullptr); + game_title = QString::fromStdString(filename); + } + + QPixmap icon = game_item->data(Qt::DecorationRole).value(); + if (icon.isNull()) { + // Use a generic icon if a game is missing one + icon = QIcon::fromTheme(QStringLiteral("application-x-executable")).pixmap(128, 128); + } + + all_games.append({ + game_title, + game_item->data(GameListItemPath::FullPathRole).toString(), + static_cast(game_item->data(GameListItemPath::ProgramIdRole).toULongLong()), + icon + }); + } + } + } + + if (all_games.empty()) { + QMessageBox::information(this, tr("Surprise Me!"), tr("No games available to choose from!")); + return; + } + + // Create and show animated dialog + SurpriseMeDialog dialog(all_games, this); + const int result = dialog.exec(); + + // If the user clicked "Launch Game"... + if (result == QDialog::Accepted) { + const SurpriseGame choice = dialog.getFinalChoice(); + if (!choice.path.isEmpty()) { + // ...then launch the game + emit GameChosen(choice.path, choice.title_id); + } + } + // If the user just closes the window (or clicks the 'X'), nothing happens. +} + +#include "game_list.moc"