mirror of
https://github.com/kamb205/KB-RPCS3.git
synced 2026-10-18 06:00:53 +02:00
1172 lines
44 KiB
C++
1172 lines
44 KiB
C++
// RPCS3 PS5 - the home screen, laid out like the PS3's XMB (cross media bar): categories in a row,
|
|
// each category's items in a column under it, a slow translucent wave behind everything and the
|
|
// clock in the corner. Drawn from scratch in that style (no Sony artwork or sounds).
|
|
// SPDX-License-Identifier: GPL-3.0-or-later
|
|
//
|
|
// Left / Right category: Settings, Game, Install, System
|
|
// Up / Down item; Cross: start / choose; Triangle: a game's options; Circle: back
|
|
// Square look for games again
|
|
//
|
|
// The previous design (UI kit "Aurora Shelf") is in git history.
|
|
|
|
#include "concepts/concepts.hpp"
|
|
#include "core/tween.hpp"
|
|
#include "ui/glyphs.hpp"
|
|
#include "ui/motion.hpp"
|
|
|
|
#include "core.hpp"
|
|
#include "library.hpp"
|
|
#include "requests.hpp"
|
|
#include "tiles.hpp"
|
|
#include "recommend.hpp"
|
|
|
|
#include <algorithm>
|
|
#include <cmath>
|
|
#include <cstdio>
|
|
#include <cstdlib>
|
|
#include <ctime>
|
|
#include <string>
|
|
#include <vector>
|
|
|
|
namespace hui::screen
|
|
{
|
|
|
|
namespace
|
|
{
|
|
|
|
using gfx::Color;
|
|
using gfx::Rect;
|
|
|
|
const Color kWhite = Color::rgb(0xffffff);
|
|
|
|
// The bar: the focused category sits at kBarX; the others follow at kCategoryGap
|
|
constexpr float kBarX = 470.0f;
|
|
constexpr float kBarY = 300.0f; // centre of the category icons
|
|
constexpr float kCategoryGap = 210.0f;
|
|
constexpr float kItemX = kBarX; // items line up under the focused category
|
|
constexpr float kFirstItemY = 470.0f; // centre of the focused item
|
|
constexpr float kItemPitch = 104.0f;
|
|
constexpr float kGameItemPitch = 150.0f;
|
|
|
|
enum Category : int
|
|
{
|
|
kSettings = 0,
|
|
kGame,
|
|
kInstall,
|
|
kSystem,
|
|
kCategoryCount
|
|
};
|
|
constexpr const char *kCategoryNames[kCategoryCount] = {"Settings", "Game", "Install", "System"};
|
|
|
|
// The Settings column: the PC version's settings tabs, each a page of RPCS3's settings in those sections
|
|
struct SettingsTab
|
|
{
|
|
const char *title;
|
|
const char *sections[2];
|
|
};
|
|
constexpr SettingsTab kTabs[] = {
|
|
{"CPU", {"Core", nullptr}}, {"GPU", {"Video", nullptr}}, {"Audio", {"Audio", nullptr}},
|
|
{"I/O", {"Input/Output", nullptr}}, {"System", {"System", nullptr}}, {"Network", {"Net", nullptr}},
|
|
{"Emulator", {"Miscellaneous", "Savestate"}},
|
|
};
|
|
constexpr int kTabCount = static_cast<int>(sizeof(kTabs) / sizeof(kTabs[0]));
|
|
constexpr const char *kVibration[] = {"Off", "Low", "Medium", "High"};
|
|
|
|
// Plain-text lines of at most `width` characters
|
|
std::vector<std::string> wrap(const std::string &text, std::size_t width)
|
|
{
|
|
std::vector<std::string> lines;
|
|
std::string line;
|
|
std::size_t at = 0;
|
|
while (at < text.size()) {
|
|
std::size_t end = text.find(' ', at);
|
|
if (end == std::string::npos)
|
|
end = text.size();
|
|
const std::string word = text.substr(at, end - at);
|
|
if (!line.empty() && line.size() + 1 + word.size() > width) {
|
|
lines.push_back(line);
|
|
line.clear();
|
|
}
|
|
line += (line.empty() ? "" : " ") + word;
|
|
at = end + 1;
|
|
}
|
|
if (!line.empty())
|
|
lines.push_back(line);
|
|
return lines;
|
|
}
|
|
|
|
constexpr const char *kTechniques[] = {
|
|
"A category bar with item columns, as on the PS3",
|
|
"Translucent wave ribbons over a slow gradient",
|
|
"Springs for every movement; game icons from ICON0.PNG",
|
|
};
|
|
|
|
// A PS3-style month colour for the background (the XMB changed its colour with the month)
|
|
Color month_colour()
|
|
{
|
|
static const std::uint32_t kMonths[12] = {0x8c8c94, 0xb89b2a, 0x5f9b30, 0xc06a92, 0x2f8f3a, 0x8a5bb8,
|
|
0x2fa0b8, 0x2d58b8, 0x7a4fb0, 0xb87a2a, 0x8a6a3a, 0xb03030};
|
|
const std::time_t now = std::time(nullptr);
|
|
std::tm local{};
|
|
localtime_r(&now, &local);
|
|
return Color::rgb(kMonths[local.tm_mon % 12]);
|
|
}
|
|
|
|
class Home final : public app::Concept
|
|
{
|
|
public:
|
|
explicit Home(app::Context &context) : context_(context)
|
|
{
|
|
theme_ = month_colour();
|
|
category_pos_.snap(static_cast<float>(category_));
|
|
}
|
|
|
|
const app::ConceptInfo &info() const override
|
|
{
|
|
static const app::ConceptInfo kInfo{
|
|
"home", "RPCS3 PS5", "The PS3 library and the emulator's home, XMB style",
|
|
"examples/rpcs3ps5/kit/home.cpp", audio::SoundSet::glass, Color::rgb(0x9ec8ff), kTechniques,
|
|
};
|
|
return kInfo;
|
|
}
|
|
|
|
void enter() override
|
|
{
|
|
age_ = 0.0f;
|
|
close_options();
|
|
}
|
|
|
|
void update(const InputFrame &input, float dt, app::Feedback &feedback) override
|
|
{
|
|
age_ += dt;
|
|
clock_ += dt;
|
|
clamp_items();
|
|
|
|
if (dialog_open_) {
|
|
update_dialog(input, feedback);
|
|
} else if (page_open_) {
|
|
update_page(input, feedback);
|
|
} else if (options_open_) {
|
|
update_options(input, feedback);
|
|
} else {
|
|
update_bar(input, feedback);
|
|
offer_new_games();
|
|
}
|
|
|
|
category_pos_.target = static_cast<float>(category_);
|
|
category_pos_.update(dt, 14.0f);
|
|
for (int c = 0; c < kCategoryCount; ++c) {
|
|
item_pos_[c].target = static_cast<float>(item_[c]);
|
|
item_pos_[c].update(dt, 16.0f);
|
|
}
|
|
options_.target = options_open_ ? 1.0f : 0.0f;
|
|
options_.update(dt, context_.settings.reduced_motion ? 40.0f : 14.0f);
|
|
option_pos_.target = static_cast<float>(option_);
|
|
option_pos_.update(dt, 22.0f);
|
|
nudge_.update(dt, 9.0f);
|
|
page_pos_.target = static_cast<float>(page_sel_);
|
|
page_pos_.update(dt, 20.0f);
|
|
}
|
|
|
|
void draw(app::Frame &frame) const override
|
|
{
|
|
// A dark gradient tinted with the month's colour; the waves are drawn on top
|
|
frame.backdrop.mode = gfx::BackdropMode::gradient;
|
|
frame.backdrop.colors[0] = gfx::mix(theme_, Color::rgb(0x000000), 0.72f);
|
|
frame.backdrop.colors[1] = gfx::mix(theme_, Color::rgb(0x000000), 0.35f);
|
|
frame.backdrop.colors[2] = gfx::mix(theme_, kWhite, 0.25f);
|
|
frame.backdrop.params[0] = 0.5f;
|
|
frame.backdrop.params[1] = 0.55f;
|
|
frame.backdrop.params[2] = 0.35f;
|
|
frame.backdrop.time = clock_;
|
|
|
|
gfx::DrawList &list = frame.scene;
|
|
draw_waves(list);
|
|
list.push_opacity(tween::stagger(age_, 0, 0.05f, 0.5f));
|
|
draw_clock(list);
|
|
draw_items(list);
|
|
draw_bar(list);
|
|
draw_status(list);
|
|
draw_hints(list);
|
|
list.pop_opacity();
|
|
if (options_.value > 0.01f)
|
|
draw_options(frame.scene);
|
|
if (page_open_)
|
|
draw_page(frame.scene);
|
|
if (dialog_open_)
|
|
draw_dialog(frame.scene);
|
|
if (page_open_ || dialog_open_)
|
|
draw_hints(frame.scene);
|
|
}
|
|
|
|
private:
|
|
static const std::vector<rpcs3ps5::Game> &games()
|
|
{
|
|
return rpcs3ps5::library().games();
|
|
}
|
|
static int game_count()
|
|
{
|
|
return static_cast<int>(games().size());
|
|
}
|
|
|
|
// ---- the items of each category ----
|
|
|
|
int item_count(int category) const
|
|
{
|
|
switch (category) {
|
|
case kSettings: return kTabCount + (rpcs3ps5::settings_for_game() ? 2 : 1);
|
|
case kGame: return std::max(game_count(), 1); // "No games" placeholder
|
|
case kInstall: return 3;
|
|
default: return 4;
|
|
}
|
|
}
|
|
|
|
void clamp_items()
|
|
{
|
|
for (int c = 0; c < kCategoryCount; ++c)
|
|
item_[c] = std::clamp(item_[c], 0, item_count(c) - 1);
|
|
}
|
|
|
|
struct Item
|
|
{
|
|
std::string label;
|
|
std::string detail;
|
|
};
|
|
|
|
Item item(int category, int index) const
|
|
{
|
|
const rpcs3ps5::Library &lib = rpcs3ps5::library();
|
|
switch (category) {
|
|
case kSettings:
|
|
if (index < kTabCount) {
|
|
const int count = tab_setting_count(index);
|
|
return {kTabs[index].title, std::to_string(count) + " settings" +
|
|
(rpcs3ps5::settings_for_game() ? " \xC2\xB7 for " + rpcs3ps5::settings_game_title() : " \xC2\xB7 all games")};
|
|
}
|
|
if (!rpcs3ps5::settings_for_game())
|
|
return {"Vibration", kVibration[rpcs3ps5::vibration_level() & 3] + std::string(" \xC2\xB7 how strongly the controller rumbles")};
|
|
if (index == kTabCount)
|
|
return {"Recommended Settings", "RPCS3's settings for this game, tuned for the PS5"};
|
|
return {"Use the global settings", rpcs3ps5::core_settings_custom(rpcs3ps5::settings_game_id())
|
|
? "This game has its own settings: Cross removes them"
|
|
: "This game uses the global settings"};
|
|
case kGame:
|
|
if (!game_count())
|
|
return {"No games yet", "Copy PS3 games into " + rpcs3ps5::Library::root() + "/games/, then press Square"};
|
|
{
|
|
const rpcs3ps5::Game &game = games()[static_cast<std::size_t>(index)];
|
|
return {game.title, game.title_id + (game.disc ? " \xC2\xB7 disc" : " \xC2\xB7 installed")};
|
|
}
|
|
case kInstall: {
|
|
static const char *kLabels[] = {"Install PS3 System Software", "Install Package Files", "Install Licences (RAP)"};
|
|
std::string detail;
|
|
if (index == 0)
|
|
detail = lib.firmware_installed()
|
|
? "Installed: " + lib.firmware_version() + (lib.firmware_pup_present() ? " \xC2\xB7 Cross reinstalls" : "")
|
|
: (lib.firmware_pup_present() ? std::string("PS3UPDAT.PUP found: Cross installs it")
|
|
: "Put PS3UPDAT.PUP in " + rpcs3ps5::Library::root());
|
|
else if (index == 1)
|
|
detail = "PKG files in " + rpcs3ps5::Library::root() + "/packages/";
|
|
else
|
|
detail = "RAP files in " + rpcs3ps5::Library::root() + "/exdata/";
|
|
return {kLabels[index], detail};
|
|
}
|
|
default: {
|
|
switch (index) {
|
|
case 0: return {"System Software", lib.firmware_installed() ? "PS3 " + lib.firmware_version() : "Not installed"};
|
|
case 1: return {"Games", std::to_string(lib.games().size()) + " found"};
|
|
case 2: return {"Data Folder", rpcs3ps5::Library::root() + " (FTP: /data/homebrew/PPSA99303/rpcs3)"};
|
|
default: return {"Emulator", std::string("RPCS3 \xC2\xB7 ") + rpcs3ps5::core_status() + " \xC2\xB7 your own console only (GPL-2.0)"};
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
// ---- every setting: the cache, the settings page, the recommended settings dialog ----
|
|
|
|
// The settings of the scope the Settings column shows (global, or the game whose settings are open)
|
|
const std::vector<rpcs3ps5::Setting> &settings() const
|
|
{
|
|
const std::string scope = rpcs3ps5::settings_for_game() ? rpcs3ps5::settings_game_id() : std::string();
|
|
if (!settings_valid_ || settings_scope_ != scope) {
|
|
settings_ = rpcs3ps5::core_settings_all(scope);
|
|
settings_scope_ = scope;
|
|
settings_valid_ = true;
|
|
}
|
|
return settings_;
|
|
}
|
|
void invalidate_settings()
|
|
{
|
|
settings_valid_ = false;
|
|
}
|
|
static bool in_tab(const rpcs3ps5::Setting &setting, int tab)
|
|
{
|
|
for (const char *section : kTabs[tab].sections)
|
|
if (section && setting.section == section)
|
|
return true;
|
|
return false;
|
|
}
|
|
int tab_setting_count(int tab) const
|
|
{
|
|
int count = 0;
|
|
for (const auto &setting : settings())
|
|
count += in_tab(setting, tab);
|
|
return count;
|
|
}
|
|
int scope_game_index() const
|
|
{
|
|
for (int i = 0; i < game_count(); ++i)
|
|
if (games()[static_cast<std::size_t>(i)].title_id == rpcs3ps5::settings_game_id())
|
|
return i;
|
|
return item_[kGame];
|
|
}
|
|
|
|
void open_page(int tab)
|
|
{
|
|
page_open_ = true;
|
|
page_tab_ = tab;
|
|
page_rows_.clear();
|
|
const auto &all = settings();
|
|
for (std::size_t i = 0; i < all.size(); ++i)
|
|
if (in_tab(all[i], tab))
|
|
page_rows_.push_back(i);
|
|
page_sel_ = 0;
|
|
page_pos_.snap(0.0f);
|
|
}
|
|
|
|
// The next (dir +1) or previous (-1) value of a setting
|
|
static std::string step_value(const rpcs3ps5::Setting &setting, int dir)
|
|
{
|
|
switch (setting.kind) {
|
|
case 'b':
|
|
return setting.value == "true" ? "false" : "true";
|
|
case 'e': {
|
|
if (setting.choices.empty())
|
|
return setting.value;
|
|
const int n = static_cast<int>(setting.choices.size());
|
|
int at = 0;
|
|
for (int i = 0; i < n; ++i)
|
|
if (setting.choices[static_cast<std::size_t>(i)] == setting.value)
|
|
at = i;
|
|
return setting.choices[static_cast<std::size_t>(((at + dir) % n + n) % n)];
|
|
}
|
|
case 'i': {
|
|
if (setting.choices.size() != 2)
|
|
return setting.value;
|
|
const long long lo = std::strtoll(setting.choices[0].c_str(), nullptr, 10);
|
|
const long long hi = std::strtoll(setting.choices[1].c_str(), nullptr, 10);
|
|
const long long range = hi - lo;
|
|
long long step = range <= 100 ? 1 : range <= 1000 ? 5 : range <= 10000 ? 50 : range / 100;
|
|
if (setting.label.find("Resolution Scale") != std::string::npos)
|
|
step = 25;
|
|
const long long value = std::clamp(std::strtoll(setting.value.c_str(), nullptr, 10) + dir * step, lo, hi);
|
|
return std::to_string(value);
|
|
}
|
|
case 'f': {
|
|
if (setting.choices.size() != 2)
|
|
return setting.value;
|
|
const double lo = std::strtod(setting.choices[0].c_str(), nullptr), hi = std::strtod(setting.choices[1].c_str(), nullptr);
|
|
const double step = (hi - lo) / 100.0;
|
|
char text[32];
|
|
std::snprintf(text, sizeof(text), "%g", std::clamp(std::strtod(setting.value.c_str(), nullptr) + dir * step, lo, hi));
|
|
return text;
|
|
}
|
|
default:
|
|
return setting.value;
|
|
}
|
|
}
|
|
|
|
void change_setting(std::size_t index, const std::string &value, app::Feedback &feedback)
|
|
{
|
|
const rpcs3ps5::Setting setting = settings()[index];
|
|
const std::string scope = rpcs3ps5::settings_for_game() ? rpcs3ps5::settings_game_id() : std::string();
|
|
if (setting.locked || setting.kind == 's') {
|
|
rpcs3ps5::set_status(setting.label + (setting.locked ? " is fixed on the PS5" : " is changed in the config file"));
|
|
feedback.play(audio::Cue::error, 1.0f, 0.0f, 0.5f);
|
|
return;
|
|
}
|
|
if (rpcs3ps5::core_setting_set(scope, setting.path, value)) {
|
|
invalidate_settings();
|
|
rpcs3ps5::set_status(setting.label + ": " + value + (scope.empty() ? " \xC2\xB7 all games" : " \xC2\xB7 " + rpcs3ps5::settings_game_title()));
|
|
feedback.play(audio::Cue::toggle);
|
|
} else {
|
|
rpcs3ps5::set_status(setting.label + ": " + value + " was not accepted");
|
|
feedback.play(audio::Cue::error, 1.0f, 0.0f, 0.5f);
|
|
}
|
|
}
|
|
|
|
void update_page(const InputFrame &input, app::Feedback &feedback)
|
|
{
|
|
const int count = static_cast<int>(page_rows_.size());
|
|
if (input.nav == Direction::up || input.nav == Direction::down || input.is_pressed(Action::page_prev) ||
|
|
input.is_pressed(Action::page_next)) {
|
|
const int move = input.nav == Direction::up ? -1 : input.nav == Direction::down ? 1 : input.is_pressed(Action::page_prev) ? -10 : 10;
|
|
const int next = std::clamp(page_sel_ + move, 0, std::max(count - 1, 0));
|
|
if (next != page_sel_) {
|
|
page_sel_ = next;
|
|
feedback.play(audio::Cue::focus, 1.0f, 0.3f);
|
|
}
|
|
}
|
|
if (count && (input.nav == Direction::left || input.nav == Direction::right || input.is_pressed(Action::confirm))) {
|
|
const std::size_t index = page_rows_[static_cast<std::size_t>(page_sel_)];
|
|
const int dir = input.nav == Direction::left ? -1 : 1;
|
|
change_setting(index, step_value(settings()[index], dir), feedback);
|
|
}
|
|
if (count && input.is_pressed(Action::west)) {
|
|
const std::size_t index = page_rows_[static_cast<std::size_t>(page_sel_)];
|
|
change_setting(index, settings()[index].def, feedback);
|
|
}
|
|
if (input.is_pressed(Action::back)) {
|
|
page_open_ = false;
|
|
feedback.play(audio::Cue::modal_close);
|
|
}
|
|
}
|
|
|
|
enum class Dialog
|
|
{
|
|
prompt, // a game was added: use its recommended settings?
|
|
details, // the changes, from the prompt
|
|
manage, // Triangle > Recommended Settings
|
|
};
|
|
|
|
void open_dialog(int game, Dialog mode)
|
|
{
|
|
if (game < 0 || game >= game_count())
|
|
return;
|
|
dialog_open_ = true;
|
|
dialog_mode_ = mode;
|
|
dialog_game_ = game;
|
|
dialog_button_ = 0;
|
|
dialog_scroll_ = 0;
|
|
recommendation_ = rpcs3ps5::recommendation_for(games()[static_cast<std::size_t>(game)]);
|
|
}
|
|
|
|
std::vector<const char *> dialog_buttons() const
|
|
{
|
|
switch (dialog_mode_) {
|
|
case Dialog::prompt: return {"Use recommended settings", "More details", "No thanks"};
|
|
case Dialog::details: return {"Use these settings", "Back"};
|
|
default: return {"Use recommended settings", "Use the global settings", "Close"};
|
|
}
|
|
}
|
|
|
|
void update_dialog(const InputFrame &input, app::Feedback &feedback)
|
|
{
|
|
const int buttons = static_cast<int>(dialog_buttons().size());
|
|
if (input.nav == Direction::left || input.nav == Direction::right) {
|
|
const int next = std::clamp(dialog_button_ + (input.nav == Direction::right ? 1 : -1), 0, buttons - 1);
|
|
if (next != dialog_button_) {
|
|
dialog_button_ = next;
|
|
feedback.play(audio::Cue::focus, 1.0f, 0.3f);
|
|
}
|
|
}
|
|
if (input.nav == Direction::up)
|
|
dialog_scroll_ = std::max(0, dialog_scroll_ - 1);
|
|
if (input.nav == Direction::down)
|
|
dialog_scroll_ = std::min(dialog_scroll_ + 1, std::max(0, static_cast<int>(recommendation_.changes.size()) - 1));
|
|
const rpcs3ps5::Game &game = games()[static_cast<std::size_t>(dialog_game_)];
|
|
const auto apply = [&] {
|
|
const bool ok = rpcs3ps5::apply_recommendation(game);
|
|
invalidate_settings();
|
|
rpcs3ps5::set_status(game.title + (ok ? ": recommended settings in use" : ": recommended settings in use (some were not accepted)"));
|
|
feedback.play(audio::Cue::launch);
|
|
dialog_open_ = false;
|
|
};
|
|
if (input.is_pressed(Action::confirm)) {
|
|
switch (dialog_mode_) {
|
|
case Dialog::prompt:
|
|
if (dialog_button_ == 0) {
|
|
apply();
|
|
} else if (dialog_button_ == 1) {
|
|
dialog_mode_ = Dialog::details;
|
|
dialog_button_ = 0;
|
|
feedback.play(audio::Cue::select);
|
|
} else {
|
|
rpcs3ps5::mark_offered(game);
|
|
rpcs3ps5::set_status(game.title + " keeps the global settings \xC2\xB7 Triangle > Recommended Settings to use them later");
|
|
feedback.play(audio::Cue::modal_close);
|
|
dialog_open_ = false;
|
|
}
|
|
break;
|
|
case Dialog::details:
|
|
if (dialog_button_ == 0) {
|
|
apply();
|
|
} else {
|
|
dialog_mode_ = Dialog::prompt;
|
|
dialog_button_ = 1;
|
|
feedback.play(audio::Cue::back);
|
|
}
|
|
break;
|
|
case Dialog::manage:
|
|
if (dialog_button_ == 0) {
|
|
apply();
|
|
} else if (dialog_button_ == 1) {
|
|
rpcs3ps5::revert_to_global(game);
|
|
invalidate_settings();
|
|
rpcs3ps5::set_status(game.title + " uses the global settings again");
|
|
feedback.play(audio::Cue::toggle);
|
|
dialog_open_ = false;
|
|
} else {
|
|
feedback.play(audio::Cue::modal_close);
|
|
dialog_open_ = false;
|
|
}
|
|
break;
|
|
}
|
|
}
|
|
if (input.is_pressed(Action::back)) {
|
|
if (dialog_mode_ == Dialog::details) {
|
|
dialog_mode_ = Dialog::prompt;
|
|
dialog_button_ = 1;
|
|
} else {
|
|
if (dialog_mode_ == Dialog::prompt)
|
|
rpcs3ps5::mark_offered(game);
|
|
dialog_open_ = false;
|
|
}
|
|
feedback.play(audio::Cue::modal_close);
|
|
}
|
|
}
|
|
|
|
// A game the library lists that was never offered its recommended settings: offered now, one at a time
|
|
void offer_new_games()
|
|
{
|
|
if (dialog_open_ || page_open_ || options_open_ || age_ < 1.0f || rpcs3ps5::tiles_busy())
|
|
return;
|
|
if (game_count() != offered_scan_count_) {
|
|
offered_scan_count_ = game_count();
|
|
pending_offers_ = rpcs3ps5::unoffered_games(games());
|
|
}
|
|
if (pending_offers_.empty())
|
|
return;
|
|
const std::size_t game = pending_offers_.front();
|
|
pending_offers_.erase(pending_offers_.begin());
|
|
if (game < games().size())
|
|
open_dialog(static_cast<int>(game), Dialog::prompt);
|
|
}
|
|
|
|
// ---- input ----
|
|
|
|
void refuse(app::Feedback &feedback, float direction)
|
|
{
|
|
feedback.play(audio::Cue::error, 1.0f, 0.0f, 0.6f);
|
|
nudge_direction_ = direction;
|
|
nudge_.trigger();
|
|
}
|
|
|
|
void update_bar(const InputFrame &input, app::Feedback &feedback)
|
|
{
|
|
if (input.nav == Direction::left || input.nav == Direction::right ||
|
|
input.is_pressed(Action::page_prev) || input.is_pressed(Action::page_next)) {
|
|
const bool right = input.nav == Direction::right || input.is_pressed(Action::page_next);
|
|
const int next = category_ + (right ? 1 : -1);
|
|
if (next >= 0 && next < kCategoryCount) {
|
|
// Leaving a game's own settings goes back to the global ones
|
|
if (category_ == kSettings && rpcs3ps5::settings_for_game())
|
|
rpcs3ps5::settings_open("", "");
|
|
category_ = next;
|
|
feedback.play(audio::Cue::tab);
|
|
} else if (!input.nav_repeat) {
|
|
refuse(feedback, right ? 1.0f : -1.0f);
|
|
}
|
|
}
|
|
if (input.nav == Direction::up || input.nav == Direction::down) {
|
|
const int next = item_[category_] + (input.nav == Direction::down ? 1 : -1);
|
|
if (next >= 0 && next < item_count(category_)) {
|
|
item_[category_] = next;
|
|
feedback.play(audio::Cue::focus);
|
|
} else if (!input.nav_repeat) {
|
|
feedback.play(audio::Cue::error, 1.0f, 0.0f, 0.4f);
|
|
}
|
|
}
|
|
if (input.is_pressed(Action::west)) {
|
|
rpcs3ps5::request(rpcs3ps5::Request::rescan);
|
|
feedback.play(audio::Cue::select);
|
|
}
|
|
if (input.is_pressed(Action::back) && category_ == kSettings && rpcs3ps5::settings_for_game()) {
|
|
// A game's settings: Circle goes back to the game
|
|
rpcs3ps5::settings_open("", "");
|
|
category_ = kGame;
|
|
feedback.play(audio::Cue::back);
|
|
}
|
|
if (input.is_pressed(Action::north) && category_ == kGame && game_count()) {
|
|
options_open_ = true;
|
|
option_ = 0;
|
|
option_pos_.snap(0.0f);
|
|
feedback.play(audio::Cue::modal_open);
|
|
}
|
|
if (input.is_pressed(Action::confirm)) {
|
|
const int index = item_[category_];
|
|
switch (category_) {
|
|
case kGame:
|
|
if (game_count()) {
|
|
feedback.play(audio::Cue::launch);
|
|
feedback.rumble(0.5f, 0.15f);
|
|
rpcs3ps5::request(rpcs3ps5::Request::boot, index);
|
|
} else {
|
|
refuse(feedback, 0.0f);
|
|
}
|
|
break;
|
|
case kInstall: {
|
|
static constexpr rpcs3ps5::Request kInstall[] = {rpcs3ps5::Request::install_firmware,
|
|
rpcs3ps5::Request::install_packages,
|
|
rpcs3ps5::Request::install_licenses};
|
|
feedback.play(audio::Cue::select);
|
|
rpcs3ps5::request(kInstall[index]);
|
|
break;
|
|
}
|
|
case kSettings:
|
|
if (index < kTabCount) {
|
|
open_page(index);
|
|
feedback.play(audio::Cue::modal_open);
|
|
} else if (!rpcs3ps5::settings_for_game()) {
|
|
rpcs3ps5::set_vibration_level((rpcs3ps5::vibration_level() + 1) % 4);
|
|
rpcs3ps5::set_status(std::string("Vibration: ") + kVibration[rpcs3ps5::vibration_level() & 3]);
|
|
feedback.play(audio::Cue::toggle);
|
|
} else if (index == kTabCount) {
|
|
open_dialog(scope_game_index(), Dialog::manage);
|
|
feedback.play(audio::Cue::modal_open);
|
|
} else {
|
|
rpcs3ps5::core_settings_clear(rpcs3ps5::settings_game_id());
|
|
invalidate_settings();
|
|
rpcs3ps5::set_status(rpcs3ps5::settings_game_title() + " uses the global settings");
|
|
feedback.play(audio::Cue::toggle);
|
|
}
|
|
break;
|
|
default:
|
|
break;
|
|
}
|
|
}
|
|
}
|
|
|
|
// The options panel's rows: a game's options, or the delete confirmation
|
|
enum Option
|
|
{
|
|
kStart,
|
|
kGameSettings,
|
|
kRecommended,
|
|
kTile,
|
|
kDelete,
|
|
kClose,
|
|
kDeleteKeepSaves,
|
|
kDeleteWithSaves,
|
|
kCancel,
|
|
};
|
|
|
|
std::vector<Option> option_rows() const
|
|
{
|
|
if (confirm_delete_)
|
|
return {kDeleteKeepSaves, kDeleteWithSaves, kCancel};
|
|
return {kStart, kGameSettings, kRecommended, kTile, kDelete, kClose};
|
|
}
|
|
|
|
const char *option_label(Option option) const
|
|
{
|
|
switch (option) {
|
|
case kStart: return "Start";
|
|
case kGameSettings: return "Game Settings";
|
|
case kRecommended: return "Recommended Settings";
|
|
case kTile: return focused_tile_present() ? "Remove from PS5 Home Screen" : "Add to PS5 Home Screen";
|
|
case kDelete: return "Delete Game";
|
|
case kDeleteKeepSaves: return "Delete, keep save data";
|
|
case kDeleteWithSaves: return "Delete, with save data";
|
|
case kCancel: return "Cancel";
|
|
default: return "Close";
|
|
}
|
|
}
|
|
|
|
bool focused_tile_present() const
|
|
{
|
|
return game_count() && rpcs3ps5::tile_present(games()[static_cast<std::size_t>(item_[kGame])]);
|
|
}
|
|
|
|
void update_options(const InputFrame &input, app::Feedback &feedback)
|
|
{
|
|
const std::vector<Option> rows = option_rows();
|
|
const int count = static_cast<int>(rows.size());
|
|
if (input.nav == Direction::up || input.nav == Direction::down) {
|
|
const int next = std::clamp(option_ + (input.nav == Direction::down ? 1 : -1), 0, count - 1);
|
|
if (next != option_) {
|
|
option_ = next;
|
|
feedback.play(audio::Cue::focus, 1.0f, 0.35f);
|
|
}
|
|
}
|
|
if (input.is_pressed(Action::confirm)) {
|
|
const int game = item_[kGame];
|
|
const rpcs3ps5::Game &g = games()[static_cast<std::size_t>(game)];
|
|
switch (rows[static_cast<std::size_t>(std::clamp(option_, 0, count - 1))]) {
|
|
case kStart:
|
|
feedback.play(audio::Cue::launch);
|
|
rpcs3ps5::request(rpcs3ps5::Request::boot, game);
|
|
break;
|
|
case kGameSettings:
|
|
// This game's own settings, in the Settings column
|
|
feedback.play(audio::Cue::select);
|
|
rpcs3ps5::settings_open(g.title_id, g.title);
|
|
category_ = kSettings;
|
|
item_[kSettings] = 0;
|
|
break;
|
|
case kRecommended:
|
|
feedback.play(audio::Cue::modal_open);
|
|
close_options();
|
|
open_dialog(game, Dialog::manage);
|
|
return;
|
|
case kTile:
|
|
feedback.play(audio::Cue::select);
|
|
rpcs3ps5::request(focused_tile_present() ? rpcs3ps5::Request::tile_remove : rpcs3ps5::Request::tile_add, game);
|
|
break;
|
|
case kDelete:
|
|
// Asks first, in the same panel
|
|
feedback.play(audio::Cue::modal_open);
|
|
confirm_delete_ = true;
|
|
option_ = 2; // Cancel
|
|
option_pos_.snap(2.0f);
|
|
return;
|
|
case kDeleteKeepSaves:
|
|
case kDeleteWithSaves:
|
|
feedback.play(audio::Cue::select);
|
|
rpcs3ps5::request(rows[static_cast<std::size_t>(option_)] == kDeleteWithSaves ? rpcs3ps5::Request::delete_game_and_saves
|
|
: rpcs3ps5::Request::delete_game,
|
|
game);
|
|
break;
|
|
default:
|
|
feedback.play(audio::Cue::modal_close);
|
|
break;
|
|
}
|
|
close_options();
|
|
}
|
|
if (input.is_pressed(Action::back) || input.is_pressed(Action::north)) {
|
|
feedback.play(audio::Cue::modal_close);
|
|
close_options();
|
|
}
|
|
}
|
|
|
|
void close_options()
|
|
{
|
|
options_open_ = false;
|
|
confirm_delete_ = false;
|
|
}
|
|
|
|
// ---- drawing ----
|
|
|
|
// Translucent ribbons drifting across the middle of the screen, as on the PS3
|
|
void draw_waves(gfx::DrawList &list) const
|
|
{
|
|
const float t = context_.settings.reduced_motion ? 0.0f : clock_;
|
|
constexpr int kSteps = 96;
|
|
const float w = static_cast<float>(gfx::kVirtualWidth);
|
|
struct Ribbon
|
|
{
|
|
float y, amp, freq, speed, phase, thick, alpha;
|
|
};
|
|
static const Ribbon kRibbons[] = {
|
|
{600, 70, 1.6f, 0.11f, 0.0f, 120, 0.07f},
|
|
{620, 55, 2.1f, -0.08f, 1.9f, 70, 0.09f},
|
|
{585, 45, 2.7f, 0.14f, 3.4f, 34, 0.12f},
|
|
};
|
|
float xy[(kSteps + 1) * 4];
|
|
for (const Ribbon &r : kRibbons) {
|
|
const auto curve = [&](float x, float offset) {
|
|
const float u = x / w;
|
|
return r.y + offset + r.amp * std::sin(u * 6.2832f * r.freq * 0.5f + t * r.speed + r.phase) +
|
|
r.amp * 0.35f * std::sin(u * 6.2832f * r.freq + t * r.speed * 1.7f + r.phase * 0.5f);
|
|
};
|
|
// Thickness swells along the ribbon, so it twists like the XMB wave
|
|
for (int i = 0; i <= kSteps; ++i) {
|
|
const float x = w * static_cast<float>(i) / kSteps;
|
|
const float swell = 0.35f + 0.65f * (0.5f + 0.5f * std::sin(x / w * 5.0f + t * r.speed * 2.3f + r.phase));
|
|
xy[i * 2] = x;
|
|
xy[i * 2 + 1] = curve(x, -r.thick * 0.5f * swell);
|
|
const int j = (kSteps + 1) * 2 - 1 - i;
|
|
xy[j * 2] = x;
|
|
xy[j * 2 + 1] = curve(x, r.thick * 0.5f * swell);
|
|
}
|
|
list.polygon(xy, (kSteps + 1) * 2, kWhite.with_alpha(r.alpha));
|
|
// A bright edge along the top of each ribbon
|
|
for (int i = 0; i < kSteps; ++i)
|
|
list.line(xy[i * 2], xy[i * 2 + 1], xy[i * 2 + 2], xy[i * 2 + 3], 1.5f, kWhite.with_alpha(r.alpha * 2.2f));
|
|
}
|
|
}
|
|
|
|
void draw_clock(gfx::DrawList &list) const
|
|
{
|
|
const std::time_t now = std::time(nullptr);
|
|
std::tm local{};
|
|
localtime_r(&now, &local);
|
|
char text[32];
|
|
std::snprintf(text, sizeof(text), "%d/%d %02d:%02d", local.tm_mday, local.tm_mon + 1, local.tm_hour, local.tm_min);
|
|
ui::text(list, context_.fonts.regular, text, gfx::kVirtualWidth - 110, 104, 30, kWhite.with_alpha(0.92f), gfx::Align::right);
|
|
list.line(gfx::kVirtualWidth - 420, 122, gfx::kVirtualWidth - 110, 122, 1.5f, kWhite.with_alpha(0.35f));
|
|
}
|
|
|
|
// The category icons, drawn from simple shapes
|
|
static void draw_category_icon(gfx::DrawList &list, int category, float cx, float cy, float s, Color ink)
|
|
{
|
|
switch (category) {
|
|
case kSettings: { // a toolbox: lid, box, handle
|
|
list.bordered_rect({cx - s * 0.5f, cy - s * 0.15f, s, s * 0.55f}, s * 0.06f, ink.with_alpha(0.0f), s * 0.07f, ink);
|
|
list.rounded_rect({cx - s * 0.5f, cy - s * 0.15f, s, s * 0.12f}, s * 0.04f, ink);
|
|
list.bordered_rect({cx - s * 0.18f, cy - s * 0.38f, s * 0.36f, s * 0.26f}, s * 0.06f, ink.with_alpha(0.0f), s * 0.07f, ink);
|
|
list.rounded_rect({cx - s * 0.06f, cy + s * 0.02f, s * 0.12f, s * 0.12f}, s * 0.02f, ink);
|
|
break;
|
|
}
|
|
case kGame: { // a pad: body, grips, d-pad, buttons
|
|
list.rounded_rect({cx - s * 0.5f, cy - s * 0.22f, s, s * 0.42f}, s * 0.2f, ink);
|
|
list.circle(cx - s * 0.36f, cy + s * 0.16f, s * 0.17f, ink);
|
|
list.circle(cx + s * 0.36f, cy + s * 0.16f, s * 0.17f, ink);
|
|
const Color hole = Color::rgb(0x000000, 0.55f);
|
|
list.rounded_rect({cx - s * 0.33f, cy - s * 0.04f, s * 0.2f, s * 0.06f}, 1, hole);
|
|
list.rounded_rect({cx - s * 0.26f, cy - s * 0.11f, s * 0.06f, s * 0.2f}, 1, hole);
|
|
list.circle(cx + s * 0.22f, cy - s * 0.06f, s * 0.04f, hole);
|
|
list.circle(cx + s * 0.3f, cy + s * 0.02f, s * 0.04f, hole);
|
|
break;
|
|
}
|
|
case kInstall: { // an arrow into a tray
|
|
list.line(cx, cy - s * 0.42f, cx, cy + s * 0.08f, s * 0.1f, ink);
|
|
const float tri[] = {cx - s * 0.2f, cy, cx + s * 0.2f, cy, cx, cy + s * 0.22f};
|
|
list.polygon(tri, 3, ink);
|
|
list.line(cx - s * 0.45f, cy + s * 0.08f, cx - s * 0.45f, cy + s * 0.36f, s * 0.08f, ink);
|
|
list.line(cx + s * 0.45f, cy + s * 0.08f, cx + s * 0.45f, cy + s * 0.36f, s * 0.08f, ink);
|
|
list.line(cx - s * 0.45f, cy + s * 0.36f, cx + s * 0.45f, cy + s * 0.36f, s * 0.08f, ink);
|
|
break;
|
|
}
|
|
default: { // a console: a slim box with a disc slot
|
|
list.bordered_rect({cx - s * 0.5f, cy - s * 0.2f, s, s * 0.4f}, s * 0.08f, ink.with_alpha(0.0f), s * 0.07f, ink);
|
|
list.line(cx - s * 0.3f, cy, cx + s * 0.1f, cy, s * 0.05f, ink);
|
|
list.circle(cx + s * 0.32f, cy, s * 0.04f, ink);
|
|
break;
|
|
}
|
|
}
|
|
}
|
|
|
|
float category_x(int category) const
|
|
{
|
|
float x = kBarX + (static_cast<float>(category) - category_pos_.value) * kCategoryGap;
|
|
// The focused category keeps clear space to its right for its label
|
|
if (static_cast<float>(category) > category_pos_.value)
|
|
x += 60.0f * std::min(1.0f, static_cast<float>(category) - category_pos_.value);
|
|
return x;
|
|
}
|
|
|
|
void draw_bar(gfx::DrawList &list) const
|
|
{
|
|
const ui::Fonts &fonts = context_.fonts;
|
|
const float shake = ui::shake(nudge_.value, clock_, 14.0f, 6.0f) * nudge_direction_;
|
|
for (int c = 0; c < kCategoryCount; ++c) {
|
|
const float x = category_x(c) + shake;
|
|
const float focus = std::max(0.0f, 1.0f - std::fabs(static_cast<float>(c) - category_pos_.value));
|
|
const float size = 70.0f + 26.0f * focus;
|
|
if (focus > 0.2f)
|
|
list.glow({x - 50, kBarY - 50, 100, 100}, 50, 40, kWhite.with_alpha(0.18f * focus));
|
|
draw_category_icon(list, c, x, kBarY, size, kWhite.with_alpha(0.5f + 0.5f * focus));
|
|
if (focus > 0.05f)
|
|
ui::text(list, fonts.regular, kCategoryNames[c], x, kBarY + 84, 26, kWhite.with_alpha(focus),
|
|
gfx::Align::center);
|
|
}
|
|
}
|
|
|
|
void draw_item_icon(gfx::DrawList &list, int category, int index, const Rect &box, float focus) const
|
|
{
|
|
if (category == kGame && game_count()) {
|
|
const rpcs3ps5::Game &game = games()[static_cast<std::size_t>(index)];
|
|
if (focus > 0.3f)
|
|
list.glow(box.inset(6), 8, 26, Color::rgb(game.accent, 0.35f * focus));
|
|
if (game.icon)
|
|
list.image(game.icon, box, gfx::kFullUv, kWhite, 6);
|
|
else
|
|
list.rounded_rect(box, 6, Color::rgb(game.mid));
|
|
return;
|
|
}
|
|
// Other items: a small round mark in the category's style
|
|
const float cx = box.cx(), cy = box.cy();
|
|
list.ring(cx, cy, 20 + 6 * focus, 3, kWhite.with_alpha(0.55f + 0.45f * focus));
|
|
list.circle(cx, cy, 7 + 3 * focus, kWhite.with_alpha(0.55f + 0.45f * focus));
|
|
}
|
|
|
|
void draw_items(gfx::DrawList &list) const
|
|
{
|
|
const ui::Fonts &fonts = context_.fonts;
|
|
// The focused category's column, and the neighbours' fading out as the bar slides
|
|
for (int c = 0; c < kCategoryCount; ++c) {
|
|
const float focus_c = std::max(0.0f, 1.0f - std::fabs(static_cast<float>(c) - category_pos_.value) * 1.6f);
|
|
if (focus_c <= 0.01f)
|
|
continue;
|
|
const bool games_column = c == kGame && game_count();
|
|
const float pitch = games_column ? kGameItemPitch : kItemPitch;
|
|
const float x = category_x(c);
|
|
const int count = item_count(c);
|
|
list.push_opacity(focus_c);
|
|
if (c == kSettings && rpcs3ps5::settings_for_game()) {
|
|
const std::string heading = "Settings for " + rpcs3ps5::settings_game_title() + " \xC2\xB7 Circle: back";
|
|
ui::text(list, fonts.semibold, heading.c_str(), x - 40, kBarY + 130, 22, kWhite.with_alpha(0.8f));
|
|
}
|
|
for (int i = 0; i < count; ++i) {
|
|
const float rel = static_cast<float>(i) - item_pos_[c].value;
|
|
const float y = item_y(rel, pitch);
|
|
if (y < -100 || y > gfx::kVirtualHeight + 100)
|
|
continue;
|
|
const float focus = std::max(0.0f, 1.0f - std::fabs(rel));
|
|
// Brightness follows the scroll too: full at the focus, dimmer below, dimmest above the bar
|
|
const float fade = rel < 0.0f ? 1.0f + (0.45f - 1.0f) * std::min(1.0f, -rel)
|
|
: 1.0f + (0.62f - 1.0f) * std::min(1.0f, rel);
|
|
list.push_opacity(fade);
|
|
const Item it = item(c, i);
|
|
if (games_column) {
|
|
const float w = 192.0f + 64.0f * focus, h = w * 176.0f / 320.0f;
|
|
draw_item_icon(list, c, i, {x - w * 0.5f, y - h * 0.5f, w, h}, focus);
|
|
ui::text(list, fonts.semibold, it.label.c_str(), x + 160 + 30 * focus, y + 4, 30 + 4 * focus, kWhite);
|
|
if (focus > 0.5f)
|
|
ui::text(list, fonts.regular, it.detail.c_str(), x + 190, y + 44, 21, kWhite.with_alpha(0.7f * focus));
|
|
} else {
|
|
draw_item_icon(list, c, i, {x - 40, y - 40, 80, 80}, focus);
|
|
ui::text(list, fonts.semibold, it.label.c_str(), x + 80, y + 2, 28 + 2 * focus, kWhite);
|
|
ui::text(list, fonts.regular, it.detail.c_str(), x + 80, y + 36, 21, kWhite.with_alpha(0.55f + 0.25f * focus));
|
|
}
|
|
list.pop_opacity();
|
|
}
|
|
list.pop_opacity();
|
|
}
|
|
}
|
|
|
|
// Where an item sits, `rel` items from the scroll position (fractional while it moves). Items after the
|
|
// focus go down the column; the one before it rises above the category bar, as on the PS3. The way up is
|
|
// one smooth curve, so an item glides past the bar instead of jumping to its row above (2026-10-07).
|
|
static float item_y(float rel, float pitch)
|
|
{
|
|
constexpr float kAboveY = kBarY - 150.0f; // the nearest item above the bar
|
|
if (rel >= 0.0f)
|
|
return kFirstItemY + rel * pitch;
|
|
if (rel >= -1.0f) {
|
|
// cubic Hermite from (rel -1, kAboveY) to (rel 0, kFirstItemY), its slopes those of the straight
|
|
// parts on either side: no jump and no change of speed as an item crosses over
|
|
const float t = rel + 1.0f, t2 = t * t, t3 = t2 * t;
|
|
const float m0 = pitch * 0.8f, m1 = pitch;
|
|
return (2 * t3 - 3 * t2 + 1) * kAboveY + (t3 - 2 * t2 + t) * m0 + (-2 * t3 + 3 * t2) * kFirstItemY + (t3 - t2) * m1;
|
|
}
|
|
return kAboveY + (rel + 1.0f) * pitch * 0.8f;
|
|
}
|
|
|
|
void draw_options(gfx::DrawList &list) const
|
|
{
|
|
// The PS3's options panel: a dark strip sliding in from the right
|
|
const ui::Fonts &fonts = context_.fonts;
|
|
const float t = options_.value;
|
|
const float w = 580.0f;
|
|
const float x = gfx::kVirtualWidth - w * tween::quint_out(t);
|
|
list.rounded_rect({0, 0, gfx::kVirtualWidth, gfx::kVirtualHeight}, 0, Color::rgb(0x000000, 0.25f * t));
|
|
list.gradient_rect_h({x, 0, w, gfx::kVirtualHeight}, 0, Color::rgb(0x000000, 0.55f * t), Color::rgb(0x000000, 0.85f * t));
|
|
list.line(x, 0, x, gfx::kVirtualHeight, 1.5f, kWhite.with_alpha(0.35f * t));
|
|
const std::vector<Option> rows = option_rows();
|
|
const float y0 = confirm_delete_ ? 520.0f : 380.0f;
|
|
list.push_opacity(t);
|
|
if (confirm_delete_ && game_count()) {
|
|
const std::string question = "Delete " + games()[static_cast<std::size_t>(item_[kGame])].title + "?";
|
|
ui::text(list, fonts.semibold, question.c_str(), x + 40, 300, 30, kWhite);
|
|
static const char *kNote[] = {"Removes the game, its installed data,", "caches and settings, and its PS5",
|
|
"home screen tile. This can't be undone."};
|
|
for (int i = 0; i < 3; ++i)
|
|
ui::text(list, fonts.regular, kNote[i], x + 40, 350 + 32.0f * i, 22, kWhite.with_alpha(0.7f));
|
|
}
|
|
list.rounded_rect({x + 30, y0 - 42 + option_pos_.value * 80, w - 60, 62}, 10, kWhite.with_alpha(0.16f));
|
|
for (std::size_t i = 0; i < rows.size(); ++i)
|
|
ui::text(list, fonts.regular, option_label(rows[i]), x + 64, y0 + static_cast<float>(i) * 80, 28,
|
|
kWhite.with_alpha(static_cast<int>(i) == option_ ? 1.0f : 0.7f));
|
|
list.pop_opacity();
|
|
}
|
|
|
|
// A settings tab: every setting of its sections, the value on the right, the selected one's details below
|
|
void draw_page(gfx::DrawList &list) const
|
|
{
|
|
const ui::Fonts &fonts = context_.fonts;
|
|
const float W = gfx::kVirtualWidth, H = gfx::kVirtualHeight;
|
|
list.rounded_rect({0, 0, W, H}, 0, Color::rgb(0x05060a, 0.88f));
|
|
const std::string scope = rpcs3ps5::settings_for_game() ? rpcs3ps5::settings_game_title() : std::string("all games");
|
|
ui::text(list, fonts.semibold, (std::string(kTabs[page_tab_].title) + " settings").c_str(), 140, 120, 44, kWhite);
|
|
ui::text(list, fonts.regular, ("For " + scope + " \xC2\xB7 " + std::to_string(page_rows_.size()) + " settings").c_str(), 140, 168, 24,
|
|
kWhite.with_alpha(0.65f));
|
|
list.line(140, 196, W - 140, 196, 1.5f, kWhite.with_alpha(0.3f));
|
|
const auto &all = settings();
|
|
constexpr float kTop = 250.0f, kPitch = 56.0f, kBottom = 860.0f;
|
|
const float scroll = std::max(0.0f, page_pos_.value - 5.0f);
|
|
for (std::size_t r = 0; r < page_rows_.size(); ++r) {
|
|
const float y = kTop + (static_cast<float>(r) - scroll) * kPitch;
|
|
if (y < kTop - kPitch * 0.5f || y > kBottom)
|
|
continue;
|
|
const rpcs3ps5::Setting &setting = all[page_rows_[r]];
|
|
const bool selected = static_cast<int>(r) == page_sel_;
|
|
if (selected)
|
|
list.rounded_rect({120, y - 36, W - 240, 52}, 10, kWhite.with_alpha(0.14f));
|
|
const float alpha = setting.locked ? 0.4f : (selected ? 1.0f : 0.78f);
|
|
ui::text(list, fonts.regular, setting.label.c_str(), 150, y, 26, kWhite.with_alpha(alpha));
|
|
std::string value = setting.kind == 'b' ? (setting.value == "true" ? "On" : "Off") : setting.value;
|
|
if (setting.locked)
|
|
value += " (fixed on PS5)";
|
|
if (selected && !setting.locked && setting.kind != 's')
|
|
value = "\xE2\x97\x80 " + value + " \xE2\x96\xB6";
|
|
ui::text(list, fonts.semibold, value.c_str(), W - 150, y, 26, kWhite.with_alpha(alpha), gfx::Align::right);
|
|
}
|
|
if (!page_rows_.empty()) {
|
|
const rpcs3ps5::Setting &setting = all[page_rows_[static_cast<std::size_t>(page_sel_)]];
|
|
list.line(140, 890, W - 140, 890, 1.5f, kWhite.with_alpha(0.3f));
|
|
std::string info = "Default: " + (setting.kind == 'b' ? std::string(setting.def == "true" ? "On" : "Off") : setting.def);
|
|
if (setting.kind == 'i' || setting.kind == 'f') {
|
|
if (setting.choices.size() == 2)
|
|
info += " \xC2\xB7 range " + setting.choices[0] + " to " + setting.choices[1];
|
|
} else if (setting.kind == 'e') {
|
|
info += " \xC2\xB7 " + std::to_string(setting.choices.size()) + " choices";
|
|
}
|
|
ui::text(list, fonts.regular, info.c_str(), 150, 935, 24, kWhite.with_alpha(0.7f));
|
|
}
|
|
}
|
|
|
|
// The recommended settings dialog: a new game's offer, its details, or Triangle > Recommended Settings
|
|
void draw_dialog(gfx::DrawList &list) const
|
|
{
|
|
const ui::Fonts &fonts = context_.fonts;
|
|
const float W = gfx::kVirtualWidth, H = gfx::kVirtualHeight;
|
|
list.rounded_rect({0, 0, W, H}, 0, Color::rgb(0x000000, 0.6f));
|
|
const Rect box{260, 130, W - 520, H - 260};
|
|
list.rounded_rect(box, 22, Color::rgb(0x12161f, 0.97f));
|
|
list.line(box.x + 40, box.y + 150, box.x + box.w - 40, box.y + 150, 1.5f, kWhite.with_alpha(0.2f));
|
|
const rpcs3ps5::Game &game = games()[static_cast<std::size_t>(dialog_game_)];
|
|
const rpcs3ps5::Recommendation &rec = recommendation_;
|
|
const float x = box.x + 60;
|
|
std::string title, subtitle;
|
|
if (dialog_mode_ == Dialog::prompt) {
|
|
title = game.title + " added";
|
|
subtitle = "Use recommended settings for it?";
|
|
} else {
|
|
title = "Recommended settings: " + game.title;
|
|
subtitle = rec.in_database ? "From RPCS3's settings for this game, plus the PS5's own" : "RPCS3 has no special settings for this game: the PS5's own only";
|
|
}
|
|
ui::text(list, fonts.semibold, title.c_str(), x, box.y + 80, 40, kWhite);
|
|
ui::text(list, fonts.regular, subtitle.c_str(), x, box.y + 126, 26, kWhite.with_alpha(0.75f));
|
|
|
|
float y = box.y + 210;
|
|
if (dialog_mode_ == Dialog::prompt) {
|
|
std::vector<std::string> lines;
|
|
if (rec.changes.empty()) {
|
|
lines.push_back("It already runs with the recommended settings: nothing to change.");
|
|
} else {
|
|
lines.push_back(std::to_string(rec.changes.size()) + " setting(s) change for this game only" +
|
|
(rec.in_database ? ", as RPCS3 recommends for it on PC," : "") + " tuned for the best performance on the PS5.");
|
|
lines.push_back("Your other games keep their settings. You can switch back at any time: Triangle > Recommended Settings.");
|
|
lines.push_back("More details lists every change and what it does.");
|
|
}
|
|
for (const std::string &text : lines)
|
|
for (const std::string &line : wrap(text, 78)) {
|
|
ui::text(list, fonts.regular, line.c_str(), x, y, 28, kWhite.with_alpha(0.9f));
|
|
y += 42;
|
|
}
|
|
} else {
|
|
if (rec.changes.empty())
|
|
ui::text(list, fonts.regular, "Nothing to change: the game already uses these settings.", x, y, 28, kWhite.with_alpha(0.9f));
|
|
const float bottom = box.y + box.h - 140;
|
|
for (std::size_t i = static_cast<std::size_t>(dialog_scroll_); i < rec.changes.size() && y < bottom; ++i) {
|
|
const rpcs3ps5::SettingChange &change = rec.changes[i];
|
|
const std::string what = change.label + ": " + (change.from.empty() ? "" : change.from + " \xE2\x86\x92 ") + change.to +
|
|
(change.fixed ? " (kept as the PS5 needs)" : "");
|
|
ui::text(list, fonts.semibold, what.c_str(), x, y, 26, kWhite.with_alpha(change.fixed ? 0.5f : 1.0f));
|
|
y += 36;
|
|
for (const std::string &line : wrap(change.why, 90)) {
|
|
ui::text(list, fonts.regular, line.c_str(), x + 24, y, 22, kWhite.with_alpha(0.65f));
|
|
y += 30;
|
|
}
|
|
y += 14;
|
|
}
|
|
if (rec.changes.size() > 3)
|
|
ui::text(list, fonts.regular, "Up / Down: more", box.x + box.w - 60, box.y + 126, 22, kWhite.with_alpha(0.5f), gfx::Align::right);
|
|
}
|
|
|
|
const std::vector<const char *> buttons = dialog_buttons();
|
|
float bx = x;
|
|
const float by = box.y + box.h - 90;
|
|
for (std::size_t i = 0; i < buttons.size(); ++i) {
|
|
const float w = 30.0f + 15.0f * static_cast<float>(std::strlen(buttons[i]));
|
|
const bool focused = static_cast<int>(i) == dialog_button_;
|
|
list.rounded_rect({bx, by - 40, w, 60}, 30, focused ? kWhite.with_alpha(0.9f) : kWhite.with_alpha(0.12f));
|
|
ui::text(list, fonts.semibold, buttons[i], bx + w * 0.5f, by, 26, focused ? Color::rgb(0x10131a) : kWhite, gfx::Align::center);
|
|
bx += w + 24;
|
|
}
|
|
}
|
|
|
|
void draw_status(gfx::DrawList &list) const
|
|
{
|
|
const std::string status = rpcs3ps5::status_text();
|
|
if (status.empty())
|
|
return;
|
|
ui::text(list, context_.fonts.regular, status.c_str(), 110, 1010, 22, kWhite.with_alpha(0.75f));
|
|
}
|
|
|
|
void draw_hints(gfx::DrawList &list) const
|
|
{
|
|
const ui::Fonts &fonts = context_.fonts;
|
|
const ui::GlyphStyle style = ui::GlyphStyle::dark();
|
|
if (dialog_open_) {
|
|
const ui::Hint hints[] = {{ui::Button::cross, "Choose"}, {ui::Button::circle, "Back"}};
|
|
ui::draw_hints(list, fonts, style, hints, 2, 1824, true);
|
|
return;
|
|
}
|
|
if (page_open_) {
|
|
const ui::Hint hints[] = {{ui::Button::cross, "Change"}, {ui::Button::square, "Default"}, {ui::Button::circle, "Back"}};
|
|
ui::draw_hints(list, fonts, style, hints, 3, 1824, true);
|
|
return;
|
|
}
|
|
if (options_open_) {
|
|
const ui::Hint hints[] = {{ui::Button::cross, "Choose"}, {ui::Button::circle, "Back"}};
|
|
ui::draw_hints(list, fonts, style, hints, 2, 1824, true);
|
|
return;
|
|
}
|
|
if (category_ == kGame && game_count()) {
|
|
const ui::Hint hints[] = {{ui::Button::cross, "Start"}, {ui::Button::triangle, "Options"}, {ui::Button::square, "Rescan"}};
|
|
ui::draw_hints(list, fonts, style, hints, 3, 1824, true);
|
|
} else {
|
|
const ui::Hint hints[] = {{ui::Button::cross, "Enter"}, {ui::Button::square, "Rescan"}};
|
|
ui::draw_hints(list, fonts, style, hints, 2, 1824, true);
|
|
}
|
|
}
|
|
|
|
app::Context &context_;
|
|
Color theme_;
|
|
int category_ = kGame;
|
|
int item_[kCategoryCount] = {};
|
|
float age_ = 0.0f;
|
|
float clock_ = 0.0f;
|
|
tween::Spring category_pos_;
|
|
tween::Spring item_pos_[kCategoryCount];
|
|
ui::Pulse nudge_;
|
|
float nudge_direction_ = 0.0f;
|
|
bool options_open_ = false;
|
|
bool confirm_delete_ = false;
|
|
mutable std::vector<rpcs3ps5::Setting> settings_;
|
|
mutable std::string settings_scope_;
|
|
mutable bool settings_valid_ = false;
|
|
bool page_open_ = false;
|
|
int page_tab_ = 0;
|
|
std::vector<std::size_t> page_rows_;
|
|
int page_sel_ = 0;
|
|
tween::Spring page_pos_;
|
|
bool dialog_open_ = false;
|
|
Dialog dialog_mode_ = Dialog::prompt;
|
|
int dialog_game_ = 0;
|
|
int dialog_button_ = 0;
|
|
int dialog_scroll_ = 0;
|
|
rpcs3ps5::Recommendation recommendation_;
|
|
int offered_scan_count_ = -1;
|
|
std::vector<std::size_t> pending_offers_;
|
|
tween::Spring options_;
|
|
int option_ = 0;
|
|
tween::Spring option_pos_;
|
|
};
|
|
|
|
} // namespace
|
|
|
|
} // namespace hui::screen
|
|
|
|
// The title's screen, for its program (ps5/ui/kit.hpp declares it)
|
|
namespace ps5ui
|
|
{
|
|
std::unique_ptr<hui::app::Concept> make_screen(hui::app::Context &context)
|
|
{
|
|
return std::make_unique<hui::screen::Home>(context);
|
|
}
|
|
} // namespace ps5ui
|