ui: achievements list, new pause menu

Fix threading issue when hashing disk for RA. Protect cacheMap with
mutex.
Achievement current challenges displayed as small icons.
Embed FontAwesome symbols.
New pause menu.
Issue #761
This commit is contained in:
Flyinghead 2024-05-03 20:09:31 +02:00
parent 2d4684a462
commit fe17d459a5
18 changed files with 2207 additions and 335 deletions

View File

@ -706,7 +706,7 @@ target_sources(${PROJECT_NAME} PRIVATE core/deps/lzma/7zArcIn.c core/deps/lzma/7
add_subdirectory(core/deps/libelf)
target_link_libraries(${PROJECT_NAME} PRIVATE elf)
if(NOT LIBRETRO)
target_compile_definitions(${PROJECT_NAME} PRIVATE IMGUI_DISABLE_DEMO_WINDOWS)
target_compile_definitions(${PROJECT_NAME} PRIVATE IMGUI_DISABLE_DEMO_WINDOWS IMGUI_DEFINE_MATH_OPERATORS)
target_include_directories(${PROJECT_NAME} PRIVATE core/deps/imgui core/deps/imgui/backends)
target_sources(${PROJECT_NAME} PRIVATE
core/deps/imgui/imgui.cpp
@ -1015,7 +1015,8 @@ cmrc_add_resources(flycast-resources
if(NOT LIBRETRO)
cmrc_add_resources(flycast-resources
fonts/Roboto-Medium.ttf.zip
fonts/Roboto-Regular.ttf.zip)
fonts/Roboto-Regular.ttf.zip
fonts/fa-solid-900.ttf.zip)
if(ANDROID)
cmrc_add_resources(flycast-resources
WHENCE resources
@ -1292,6 +1293,7 @@ target_sources(${PROJECT_NAME} PRIVATE
if(NOT LIBRETRO)
target_sources(${PROJECT_NAME} PRIVATE
core/rend/game_scanner.h
core/rend/imgui_driver.cpp
core/rend/imgui_driver.h
core/rend/gui.cpp
core/rend/gui.h

View File

@ -36,6 +36,7 @@
#include <unordered_map>
#include <sstream>
#include <atomic>
#include <tuple>
#include <xxhash.h>
namespace achievements
@ -51,6 +52,9 @@ public:
std::future<void> login(const char *username, const char *password);
void logout();
bool isLoggedOn() const { return loggedOn; }
bool isActive() const { return active; }
Game getCurrentGame();
std::vector<Achievement> getAchievementList();
void serialize(Serializer& ser);
void deserialize(Deserializer& deser);
@ -66,6 +70,7 @@ private:
void resumeGame();
void loadCache();
std::string getOrDownloadImage(const char *url);
std::tuple<std::string, bool> getCachedImage(const char *url);
void diskChange();
static void clientLoginWithTokenCallback(int result, const char *error_message, rc_client_t *client, void *userdata);
@ -96,6 +101,8 @@ private:
cResetEvent resetEvent;
std::string cachePath;
std::unordered_map<u64, std::string> cacheMap;
std::mutex cacheMutex;
std::future<void> asyncImageDownload;
};
bool init()
@ -123,6 +130,21 @@ bool isLoggedOn()
return Achievements::Instance().isLoggedOn();
}
bool isActive()
{
return Achievements::Instance().isActive();
}
Game getCurrentGame()
{
return Achievements::Instance().getCurrentGame();
}
std::vector<Achievement> getAchievementList()
{
return Achievements::Instance().getAchievementList();
}
void serialize(Serializer& ser)
{
Achievements::Instance().serialize(ser);
@ -224,39 +246,63 @@ void Achievements::loadCache()
continue;
std::string s = get_file_basename(name);
u64 v = strtoull(s.c_str(), nullptr, 16);
std::lock_guard<std::mutex> _(cacheMutex);
cacheMap[v] = name;
}
closedir(dir);
}
}
static u64 hashUrl(const char *url) {
return XXH64(url, strlen(url), 13);
}
std::tuple<std::string, bool> Achievements::getCachedImage(const char *url)
{
u64 hash = hashUrl(url);
std::lock_guard<std::mutex> _(cacheMutex);
auto it = cacheMap.find(hash);
if (it != cacheMap.end()) {
return make_tuple(cachePath + it->second, true);
}
else
{
std::stringstream stream;
stream << std::hex << hash << ".png";
return make_tuple(cachePath + stream.str(), false);
}
}
std::string Achievements::getOrDownloadImage(const char *url)
{
u64 hash = XXH64(url, strlen(url), 13);
auto it = cacheMap.find(hash);
if (it != cacheMap.end())
return cachePath + it->second;
u64 hash = hashUrl(url);
{
std::lock_guard<std::mutex> _(cacheMutex);
auto it = cacheMap.find(hash);
if (it != cacheMap.end())
return cachePath + it->second;
}
std::vector<u8> content;
std::string content_type;
int rc = http::get(url, content, content_type);
if (!http::success(rc))
return {};
std::stringstream stream;
stream << std::hex << hash;
if (content_type == "image/jpeg")
stream << ".jpg";
else
stream << ".png";
std::string path = cachePath + stream.str();
FILE *f = nowide::fopen(path.c_str(), "wb");
stream << std::hex << hash << ".png";
std::string localPath = cachePath + stream.str();
FILE *f = nowide::fopen(localPath.c_str(), "wb");
if (f == nullptr) {
WARN_LOG(COMMON, "Can't save image to %s", path.c_str());
WARN_LOG(COMMON, "Can't save image to %s", localPath.c_str());
return {};
}
fwrite(content.data(), 1, content.size(), f);
fclose(f);
cacheMap[hash] = stream.str();
DEBUG_LOG(COMMON, "RA: downloaded %s to %s", url, path.c_str());
return path;
{
std::lock_guard<std::mutex> _(cacheMutex);
cacheMap[hash] = stream.str();
}
DEBUG_LOG(COMMON, "RA: downloaded %s to %s", url, localPath.c_str());
return localPath;
}
void Achievements::term()
@ -264,6 +310,8 @@ void Achievements::term()
if (rc_client == nullptr)
return;
unloadGame();
if (asyncImageDownload.valid())
asyncImageDownload.get();
rc_client_destroy(rc_client);
rc_client = nullptr;
}
@ -507,15 +555,26 @@ void Achievements::handleUnlockEvent(const rc_client_event_t *event)
void Achievements::handleAchievementChallengeIndicatorShowEvent(const rc_client_event_t *event)
{
// TODO there might be more than one. Need to display an icon.
//std::string msg = "Challenge: " + std::string(event->achievement->title);
//gui_display_notification(msg.c_str(), 10000);
INFO_LOG(COMMON, "RA: Challenge: %s", event->achievement->title);
char url[128];
int rc = rc_client_achievement_get_image_url(event->achievement, RC_CLIENT_ACHIEVEMENT_STATE_UNLOCKED, url, sizeof(url));
if (rc == RC_OK)
{
std::string image = getOrDownloadImage(url);
notifier.showChallenge(image);
}
}
void Achievements::handleAchievementChallengeIndicatorHideEvent(const rc_client_event_t *event)
{
// TODO
INFO_LOG(COMMON, "RA: Challenge hidden: %s", event->achievement->title);
char url[128];
int rc = rc_client_achievement_get_image_url(event->achievement, RC_CLIENT_ACHIEVEMENT_STATE_UNLOCKED, url, sizeof(url));
if (rc == RC_OK)
{
std::string image = getOrDownloadImage(url);
notifier.hideChallenge(image);
}
}
void Achievements::handleGameCompleted(const rc_client_event_t *event)
@ -551,6 +610,7 @@ void Achievements::handleUpdateAchievementProgress(const rc_client_event_t *even
notifier.notify(Notification::Progress, image, event->achievement->measured_progress);
}
static Disc *hashDisk;
static bool add150;
static void *cdreader_open_track(const char* path, u32 track)
@ -558,54 +618,31 @@ static void *cdreader_open_track(const char* path, u32 track)
DEBUG_LOG(COMMON, "RA: cdreader_open_track %s track %d", path, track);
if (track == RC_HASH_CDTRACK_FIRST_DATA)
{
u32 toc[102];
libGDR_GetToc(toc, SingleDensity);
for (int i = 0; i < 99; i++)
if (toc[i] != 0xffffffff)
{
if (((toc[i] >> 4) & 0xf) & 4)
return reinterpret_cast<void *>(i + 1);
}
if (libGDR_GetDiscType() == GdRom)
{
libGDR_GetToc(toc, DoubleDensity);
for (int i = 0; i < 99; i++)
if (toc[i] != 0xffffffff)
{
if (((toc[i] >> 4) & 0xf) & 4)
return reinterpret_cast<void *>(i + 1);
}
}
}
u32 start, end;
if (!libGDR_GetTrack(track, start, end))
for (const Track& track : hashDisk->tracks)
if (track.isDataTrack())
return const_cast<Track *>(&track);
return nullptr;
}
if (track <= hashDisk->tracks.size())
return const_cast<Track *>(&hashDisk->tracks[track - 1]);
else
return reinterpret_cast<void *>(track);
return nullptr;
}
static size_t cdreader_read_sector(void* track_handle, u32 sector, void* buffer, size_t requested_bytes)
{
//DEBUG_LOG(COMMON, "RA: cdreader_read_sector track %zd sec %d num %zd", reinterpret_cast<uintptr_t>(track_handle), sector, requested_bytes);
if (requested_bytes == 2048)
// add 150 sectors to FAD corresponding to files
// FIXME get rid of this
add150 = true;
if (add150) // FIXME how to get rid of this?
//DEBUG_LOG(COMMON, "RA: cdreader_read_sector track %p sec %d+%d num %zd", track_handle, sector, add150 ? 150 : 0, requested_bytes);
if (add150)
sector += 150;
u32 count = requested_bytes;
u32 secNum = count / 2048;
if (secNum > 0)
{
libGDR_ReadSector((u8 *)buffer, sector, secNum, 2048);
buffer = (u8 *)buffer + secNum * 2048;
count -= secNum * 2048;
}
if (count > 0)
{
u8 locbuf[2048];
libGDR_ReadSector(locbuf, sector + secNum, 1, 2048);
memcpy(buffer, locbuf, count);
}
u8 locbuf[2048];
hashDisk->ReadSectors(sector, 1, locbuf, 2048);
requested_bytes = std::min<size_t>(requested_bytes, 2048);
memcpy(buffer, locbuf, requested_bytes);
return requested_bytes;
}
@ -615,12 +652,9 @@ static void cdreader_close_track(void* track_handle)
static u32 cdreader_first_track_sector(void* track_handle)
{
u32 trackNum = reinterpret_cast<uintptr_t>(track_handle);
u32 start, end;
if (!libGDR_GetTrack(trackNum, start, end))
return 0;
DEBUG_LOG(COMMON, "RA: cdreader_first_track_sector track %d -> %d", trackNum, start);
return start;
Track& track = *static_cast<Track *>(track_handle);
DEBUG_LOG(COMMON, "RA: cdreader_first_track_sector track %p -> %d", track_handle, track.StartFAD);
return track.StartFAD;
}
std::string Achievements::getGameHash()
@ -629,7 +663,13 @@ std::string Achievements::getGameHash()
{
const u32 diskType = libGDR_GetDiscType();
if (diskType == NoDisk || diskType == Open)
return "";
return {};
// Reopen the disk locally to avoid threading issues (CHD)
try {
hashDisk = OpenDisc(settings.content.path);
} catch (const FlycastException& e) {
return {};
}
add150 = false;
rc_hash_cdreader hooks = {
cdreader_open_track,
@ -641,7 +681,7 @@ std::string Achievements::getGameHash()
rc_hash_init_error_message_callback([](const char *msg) {
WARN_LOG(COMMON, "cdreader: %s", msg);
});
#ifndef NDEBUG
#if !defined(NDEBUG) || defined(DEBUGFAST)
rc_hash_init_verbose_message_callback([](const char *msg) {
DEBUG_LOG(COMMON, "cdreader: %s", msg);
});
@ -650,6 +690,8 @@ std::string Achievements::getGameHash()
char hash[33] {};
rc_hash_generate_from_file(hash, settings.platform.isConsole() ? RC_CONSOLE_DREAMCAST : RC_CONSOLE_ARCADE,
settings.content.fileName.c_str()); // fileName is only used for arcade
delete hashDisk;
hashDisk = nullptr;
return hash;
}
@ -724,7 +766,7 @@ void Achievements::loadGame()
}
if (!init() || !isLoggedOn()) {
if (!isLoggedOn())
WARN_LOG(COMMON, "Not logged on. Not loading game yet");
INFO_LOG(COMMON, "Not logged on. Not loading game yet");
loadingGame = false;
return;
}
@ -735,6 +777,10 @@ void Achievements::loadGame()
((Achievements *)userdata)->gameLoaded(result, error_message);
}, this);
}
else {
INFO_LOG(COMMON, "RA: empty hash. Aborting load");
loadingGame = false;
}
}
void Achievements::gameLoaded(int result, const char *errorMessage)
@ -811,6 +857,93 @@ void Achievements::diskChange()
}, this);
}
Game Achievements::getCurrentGame()
{
if (!active)
return Game{};
const rc_client_game_t *info = rc_client_get_game_info(rc_client);
if (info == nullptr)
return Game{};
char url[128];
std::string image;
if (rc_client_game_get_image_url(info, url, sizeof(url)) == RC_OK)
{
bool cached;
std::tie(image, cached) = getCachedImage(url);
if (!cached)
{
if (asyncImageDownload.valid())
{
if (asyncImageDownload.wait_for(std::chrono::seconds::zero()) == std::future_status::timeout)
INFO_LOG(COMMON, "Async image download already in progress");
else
asyncImageDownload.get();
}
if (!asyncImageDownload.valid())
{
std::string surl = url;
asyncImageDownload = std::async(std::launch::async, [this, surl]() {
getOrDownloadImage(surl.c_str());
});
}
}
}
rc_client_user_game_summary_t summary;
rc_client_get_user_game_summary(rc_client, &summary);
return Game{ image, info->title, summary.num_unlocked_achievements, summary.num_core_achievements, summary.points_unlocked, summary.points_core };
}
std::vector<Achievement> Achievements::getAchievementList()
{
std::vector<Achievement> achievements;
rc_client_achievement_list_t *list = rc_client_create_achievement_list(rc_client,
RC_CLIENT_ACHIEVEMENT_CATEGORY_CORE_AND_UNOFFICIAL,
RC_CLIENT_ACHIEVEMENT_LIST_GROUPING_PROGRESS);
std::vector<std::string> uncachedImages;
for (u32 i = 0; i < list->num_buckets; i++)
{
const char *label = list->buckets[i].label;
for (u32 j = 0; j < list->buckets[i].num_achievements; j++)
{
const rc_client_achievement_t *achievement = list->buckets[i].achievements[j];
char url[128];
std::string image;
if (rc_client_achievement_get_image_url(achievement, achievement->state, url, sizeof(url)) == RC_OK)
{
bool cached;
std::tie(image, cached) = getCachedImage(url);
if (!cached)
uncachedImages.push_back(url);
}
std::string status;
if (achievement->measured_percent)
status = achievement->measured_progress;
achievements.emplace_back(image, achievement->title, achievement->description, label, status);
}
}
rc_client_destroy_achievement_list(list);
if (!uncachedImages.empty())
{
if (asyncImageDownload.valid())
{
if (asyncImageDownload.wait_for(std::chrono::seconds::zero()) == std::future_status::timeout)
INFO_LOG(COMMON, "Async image download already in progress");
else
asyncImageDownload.get();
}
if (!asyncImageDownload.valid())
{
asyncImageDownload = std::async(std::launch::async, [this, uncachedImages]() {
for (const std::string& url : uncachedImages)
getOrDownloadImage(url.c_str());
});
}
}
return achievements;
}
void Achievements::serialize(Serializer& ser)
{
u32 size = (u32)rc_client_progress_size(rc_client);

View File

@ -17,16 +17,42 @@
#pragma once
#include "types.h"
#include <future>
#include <vector>
namespace achievements
{
#ifdef USE_RACHIEVEMENTS
struct Game
{
std::string image;
std::string title;
u32 unlockedAchievements;
u32 totalAchievements;
u32 points;
u32 totalPoints;
};
struct Achievement
{
Achievement() = default;
Achievement(const std::string& image, const std::string& title, const std::string& description, const std::string& category, const std::string& status)
: image(image), title(title), description(description), category(category), status(status) {}
std::string image;
std::string title;
std::string description;
std::string category;
std::string status;
};
bool init();
void term();
std::future<void> login(const char *username, const char *password);
void logout();
bool isLoggedOn();
bool isActive();
Game getCurrentGame();
std::vector<Achievement> getAchievementList();
#endif

View File

@ -38,6 +38,7 @@ void dc_exit();
void dc_savestate(int index = 0);
void dc_loadstate(int index = 0);
void dc_loadstate(Deserializer& deser);
std::string dc_getStateUpdateDate(int index);
enum class Event {
Start,

View File

@ -7,6 +7,7 @@
#include "log/LogManager.h"
#include "rend/gui.h"
#include "oslib/oslib.h"
#include "oslib/directory.h"
#include "debug/gdb_server.h"
#include "archive/rzip.h"
#include "rend/mainui.h"
@ -14,6 +15,7 @@
#include "lua/lua.h"
#include "stdclass.h"
#include "serialize.h"
#include <time.h>
int flycast_init(int argc, char* argv[])
{
@ -231,4 +233,32 @@ void dc_loadstate(int index)
EventManager::event(Event::LoadState);
}
#ifdef _WIN32
static struct tm *localtime_r(const time_t *_clock, struct tm *_result)
{
return localtime_s(_result, _clock) ? nullptr : _result;
}
#endif
std::string dc_getStateUpdateDate(int index)
{
std::string filename = hostfs::getSavestatePath(index, false);
struct stat st;
if (flycast::stat(filename.c_str(), &st) != 0)
return {};
time_t ago = time(nullptr) - st.st_mtime;
if (ago < 60)
return std::to_string(ago) + " seconds ago";
if (ago < 3600)
return std::to_string(ago / 60) + " minutes ago";
if (ago < 3600 * 24)
return std::to_string(ago / 3600) + " hours ago";
tm t;
if (localtime_r(&st.st_mtime, &t) == nullptr)
return {};
std::string s(32, '\0');
s.resize(snprintf(s.data(), 32, "%04d/%02d/%02d %02d:%02d:%02d", t.tm_year + 1900, t.tm_mon + 1, t.tm_mday, t.tm_hour, t.tm_min, t.tm_sec));
return s;
}
#endif

View File

@ -184,7 +184,8 @@ public:
#ifndef _WIN32
struct stat st;
if (flycast::stat(path.c_str(), &st) != 0) {
INFO_LOG(COMMON, "Cannot stat file '%s' errno %d", path.c_str(), errno);
if (errno != ENOENT)
INFO_LOG(COMMON, "Cannot stat file '%s' errno %d", path.c_str(), errno);
throw StorageException("Cannot stat " + path);
}
info.isDirectory = S_ISDIR(st.st_mode);

View File

@ -284,7 +284,7 @@ void CustomTexture::DumpTexture(u32 hash, int w, int h, TextureType textype, voi
FILE *f = nowide::fopen((const char *)context, "wb");
if (f == nullptr)
{
WARN_LOG(RENDERER, "Dump texture: can't save to file %s: error %d", context, errno);
WARN_LOG(RENDERER, "Dump texture: can't save to file %s: error %d", (const char *)context, errno);
}
else
{

File diff suppressed because it is too large Load Diff

View File

@ -48,6 +48,7 @@
#include "oslib/resources.h"
#include "achievements/achievements.h"
#include "gui_achievements.h"
#include "IconsFontAwesome6.h"
#if defined(USE_SDL)
#include "sdl/sdl.h"
#endif
@ -309,6 +310,12 @@ void gui_initFonts()
// TODO Linux, iOS, ...
#endif
// Font Awesome symbols (added to default font)
data = resource::load("fonts/" FONT_ICON_FILE_NAME_FAS, dataSize);
verify(data != nullptr);
const float symbolFontSize = 21.f * settings.display.uiScale;
static ImWchar faRanges[] = { ICON_MIN_FA, ICON_MAX_FA, 0 };
io.Fonts->AddFontFromMemoryTTF(data.release(), dataSize, symbolFontSize, &font_cfg, faRanges);
// Large font without Asian glyphs
data = resource::load("fonts/Roboto-Regular.ttf", dataSize);
verify(data != nullptr);
@ -574,104 +581,159 @@ static bool savestateAllowed()
static void gui_display_commands()
{
imguiDriver->displayVmus();
imguiDriver->displayVmus();
fullScreenWindow(false);
ImGui::SetNextWindowBgAlpha(0.8f);
ImguiStyleVar _{ImGuiStyleVar_WindowBorderSize, 0};
centerNextWindow();
ImGui::SetNextWindowSize(ScaledVec2(330, 0));
ImGui::Begin("##commands", NULL, ImGuiWindowFlags_NoDecoration);
{
ImguiStyleVar _{ImGuiStyleVar_ButtonTextAlign, ImVec2(0.f, 0.5f)}; // left aligned
ImGui::Begin("##commands", NULL, ImGuiWindowFlags_NoResize | ImGuiWindowFlags_NoTitleBar | ImGuiWindowFlags_NoMove | ImGuiWindowFlags_AlwaysAutoResize);
float buttonHeight = 50.f; // not scaled
bool lowH = ImGui::GetContentRegionAvail().y < ((100 + 50 * 6) * settings.display.uiScale
+ ImGui::GetStyle().FramePadding.y * 2 + ImGui::GetStyle().ItemSpacing.y * 5);
if (lowH)
{
// Low height available (phone): Put game icon in first column without text
// Button columns in next 2 columns
float emptyW = ImGui::GetContentRegionAvail().x - (100 + 150 * 2) * settings.display.uiScale - ImGui::GetStyle().WindowPadding.x * 2;
ImGui::Columns(3, "buttons", false);
ImGui::SetColumnWidth(0, 100 * settings.display.uiScale + ImGui::GetStyle().FramePadding.x * 2 + emptyW / 3);
bool veryLowH = ImGui::GetContentRegionAvail().y < (50 * 6 * settings.display.uiScale
+ ImGui::GetStyle().ItemSpacing.y * 5);
if (veryLowH)
buttonHeight = (ImGui::GetContentRegionAvail().y - ImGui::GetStyle().ItemSpacing.y * 5)
/ 6 / settings.display.uiScale;
}
GameMedia game;
game.path = settings.content.path;
game.fileName = settings.content.fileName;
GameBoxart art = boxart.getBoxart(game);
ImguiTexture tex(art.boxartPath);
// TODO use placeholder image if not available
tex.draw(ScaledVec2(100, 100));
{
if (card_reader::barcodeAvailable())
{
if (!lowH)
{
ImGui::SameLine();
ImGui::BeginChild("game_info", ScaledVec2(0, 100.f), ImGuiChildFlags_Border, ImGuiWindowFlags_None);
ImGui::PushFont(largeFont);
ImGui::Text("%s", art.name.c_str());
ImGui::PopFont();
ImGui::PushStyleColor(ImGuiCol_Text, ImVec4(0.75f, 0.75f, 0.75f, 1.f));
ImGui::TextWrapped("%s", art.fileName.c_str());
ImGui::PopStyleColor();
ImGui::EndChild();
ImGui::Columns(3, "buttons", false);
ImGui::SetColumnWidth(0, 100.f * settings.display.uiScale + ImGui::GetStyle().ItemSpacing.x);
ImGui::SetColumnWidth(1, 200.f * settings.display.uiScale);
}
ImGui::NextColumn();
ImguiStyleVar _1{ImGuiStyleVar_FramePadding, ScaledVec2(12.f, 3.f)};
// Resume
if (ImGui::Button(ICON_FA_PLAY " Resume", ScaledVec2(150, buttonHeight)))
{
GamepadDevice::load_system_mappings();
gui_setState(GuiState::Closed);
}
// Cheats
{
DisabledScope _{settings.network.online};
if (ImGui::Button(ICON_FA_MASK " Cheats", ScaledVec2(150, buttonHeight)) && !settings.network.online)
gui_setState(GuiState::Cheats);
}
// Achievements
{
DisabledScope _{!achievements::isActive()};
if (ImGui::Button(ICON_FA_TROPHY " Achievements", ScaledVec2(150, buttonHeight)) && achievements::isActive())
gui_setState(GuiState::Achievements);
}
// Insert/Eject Disk
const char *disk_label = libGDR_GetDiscType() == Open ? ICON_FA_COMPACT_DISC " Insert Disk" : ICON_FA_COMPACT_DISC " Eject Disk";
if (ImGui::Button(disk_label, ScaledVec2(150, buttonHeight)))
{
if (libGDR_GetDiscType() == Open) {
gui_setState(GuiState::SelectDisk);
}
else {
DiscOpenLid();
gui_setState(GuiState::Closed);
}
}
// Settings
if (ImGui::Button(ICON_FA_GEAR " Settings", ScaledVec2(150, buttonHeight)))
gui_setState(GuiState::Settings);
// Exit
if (ImGui::Button(commandLineStart ? ICON_FA_POWER_OFF " Exit" : ICON_FA_POWER_OFF " Close Game", ScaledVec2(150, buttonHeight)))
gui_stop_game();
ImGui::NextColumn();
{
DisabledScope _{!savestateAllowed()};
// Load State
if (ImGui::Button(ICON_FA_CLOCK_ROTATE_LEFT " Load State", ScaledVec2(150, buttonHeight)) && savestateAllowed())
{
gui_setState(GuiState::Closed);
dc_loadstate(config::SavestateSlot);
}
// Save State
if (ImGui::Button(ICON_FA_DOWNLOAD " Save State", ScaledVec2(150, buttonHeight)) && savestateAllowed())
{
gui_setState(GuiState::Closed);
dc_savestate(config::SavestateSlot);
}
// Slot #
if (ImGui::ArrowButton("##prev-slot", ImGuiDir_Left))
{
if (config::SavestateSlot == 0)
config::SavestateSlot = 9;
else
config::SavestateSlot--;
SaveSettings();
}
std::string slot = "Slot " + std::to_string((int)config::SavestateSlot + 1);
float spacingW = (150.f * settings.display.uiScale - ImGui::GetFrameHeight() * 2 - ImGui::CalcTextSize(slot.c_str()).x) / 2;
ImGui::SameLine(0, spacingW);
ImGui::Text("%s", slot.c_str());
ImGui::SameLine(0, spacingW);
if (ImGui::ArrowButton("##next-slot", ImGuiDir_Right))
{
if (config::SavestateSlot == 9)
config::SavestateSlot = 0;
else
config::SavestateSlot++;
SaveSettings();
}
std::string date = dc_getStateUpdateDate(config::SavestateSlot);
{
ImVec4 gray(0.75f, 0.75f, 0.75f, 1.f);
if (date.empty())
ImGui::TextColored(gray, "Empty");
else
ImGui::TextColored(gray, "%s", date.c_str());
}
}
// Barcode
if (card_reader::barcodeAvailable())
{
ImGui::NewLine();
ImGui::Text("Barcode Card");
char cardBuf[64] {};
strncpy(cardBuf, card_reader::barcodeGetCard().c_str(), sizeof(cardBuf) - 1);
if (ImGui::InputText("Card", cardBuf, sizeof(cardBuf), ImGuiInputTextFlags_None, nullptr, nullptr))
if (ImGui::InputText("##barcode", cardBuf, sizeof(cardBuf), ImGuiInputTextFlags_None, nullptr, nullptr))
card_reader::barcodeSetCard(cardBuf);
}
DisabledScope scope(!savestateAllowed());
// Load State
if (ImGui::Button("Load State", ScaledVec2(110, 50)) && savestateAllowed())
{
gui_setState(GuiState::Closed);
dc_loadstate(config::SavestateSlot);
}
ImGui::SameLine();
// Slot #
std::string slot = "Slot " + std::to_string((int)config::SavestateSlot + 1);
if (ImGui::Button(slot.c_str(), ImVec2(80 * settings.display.uiScale - ImGui::GetStyle().FramePadding.x, 50 * settings.display.uiScale)))
ImGui::OpenPopup("slot_select_popup");
if (ImGui::BeginPopup("slot_select_popup"))
{
for (int i = 0; i < 10; i++)
if (ImGui::Selectable(std::to_string(i + 1).c_str(), config::SavestateSlot == i, 0,
ImVec2(ImGui::CalcTextSize("Slot 8").x, 0))) {
config::SavestateSlot = i;
SaveSettings();
}
ImGui::EndPopup();
}
ImGui::SameLine();
// Save State
if (ImGui::Button("Save State", ScaledVec2(110, 50)) && savestateAllowed())
{
gui_setState(GuiState::Closed);
dc_savestate(config::SavestateSlot);
}
}
ImGui::Columns(2, "buttons", false);
// Settings
if (ImGui::Button("Settings", ScaledVec2(150, 50)))
{
gui_setState(GuiState::Settings);
ImGui::Columns(1, nullptr, false);
}
ImGui::NextColumn();
if (ImGui::Button("Resume", ScaledVec2(150, 50)))
{
GamepadDevice::load_system_mappings();
gui_setState(GuiState::Closed);
}
ImGui::NextColumn();
// Insert/Eject Disk
const char *disk_label = libGDR_GetDiscType() == Open ? "Insert Disk" : "Eject Disk";
if (ImGui::Button(disk_label, ScaledVec2(150, 50)))
{
if (libGDR_GetDiscType() == Open)
{
gui_setState(GuiState::SelectDisk);
}
else
{
DiscOpenLid();
gui_setState(GuiState::Closed);
}
}
ImGui::NextColumn();
// Cheats
{
DisabledScope scope(settings.network.online);
if (ImGui::Button("Cheats", ScaledVec2(150, 50)) && !settings.network.online)
gui_setState(GuiState::Cheats);
}
ImGui::Columns(1, nullptr, false);
// Exit
if (ImGui::Button(commandLineStart ? "Exit" : "Close Game", ScaledVec2(300, 50)
+ ImVec2(ImGui::GetStyle().ColumnsMinSpacing + ImGui::GetStyle().FramePadding.x * 2 - 1, 0)))
{
gui_stop_game();
}
ImGui::End();
}
@ -2929,49 +2991,9 @@ static void gameTooltip(const std::string& tip)
}
}
static bool getGameImage(const GameBoxart& art, ImTextureID& textureId, bool allowLoad)
static bool gameImageButton(ImguiTexture& texture, const std::string& tooltip, ImVec2 size, const std::string& gameName)
{
textureId = ImTextureID{};
if (art.boxartPath.empty())
return false;
// Get the boxart texture. Load it if needed.
textureId = imguiDriver->getTexture(art.boxartPath);
if (textureId == ImTextureID() && allowLoad)
{
int width, height;
u8 *imgData = loadImage(art.boxartPath, width, height);
if (imgData != nullptr)
{
try {
textureId = imguiDriver->updateTextureAndAspectRatio(art.boxartPath, imgData, width, height);
} catch (...) {
// vulkan can throw during resizing
}
free(imgData);
}
return true;
}
return false;
}
static bool gameImageButton(ImTextureID textureId, const std::string& tooltip, ImVec2 size)
{
float ar = imguiDriver->getAspectRatio(textureId);
ImVec2 uv0 { 0.f, 0.f };
ImVec2 uv1 { 1.f, 1.f };
if (ar > 1)
{
uv0.y = -(ar - 1) / 2;
uv1.y = 1 + (ar - 1) / 2;
}
else if (ar != 0)
{
ar = 1 / ar;
uv0.x = -(ar - 1) / 2;
uv1.x = 1 + (ar - 1) / 2;
}
bool pressed = ImGui::ImageButton("", textureId, size - ImGui::GetStyle().FramePadding * 2, uv0, uv1);
bool pressed = texture.button("", size, gameName);
gameTooltip(tooltip);
return pressed;
@ -3036,22 +3058,16 @@ static void gui_display_content()
ImGui::PushStyleVar(ImGuiStyleVar_ItemSpacing, ScaledVec2(8, 20));
int counter = 0;
int loadedImages = 0;
if (gui_state != GuiState::SelectDisk && filter.PassFilter("Dreamcast BIOS"))
{
ImGui::PushID("bios");
bool pressed;
if (config::BoxartDisplayMode)
{
ImTextureID textureId{};
GameMedia game;
GameBoxart art = boxart.getBoxartAndLoad(game);
if (getGameImage(art, textureId, loadedImages < 10))
loadedImages++;
if (textureId != ImTextureID())
pressed = gameImageButton(textureId, "Dreamcast BIOS", responsiveBoxVec2);
else
pressed = ImGui::Button("Dreamcast BIOS", responsiveBoxVec2);
ImguiTexture tex(art.boxartPath);
pressed = gameImageButton(tex, "Dreamcast BIOS", responsiveBoxVec2, "Dreamcast BIOS");
}
else
{
@ -3084,23 +3100,19 @@ static void gui_display_content()
if (filter.PassFilter(gameName.c_str()))
{
ImGui::PushID(game.path.c_str());
bool pressed;
bool pressed = false;
if (config::BoxartDisplayMode)
{
if (counter % itemsPerLine != 0)
ImGui::SameLine();
counter++;
ImTextureID textureId{};
// Get the boxart texture. Load it if needed (max 10 per frame).
if (getGameImage(art, textureId, loadedImages < 10))
loadedImages++;
if (textureId != ImTextureID())
pressed = gameImageButton(textureId, game.name, responsiveBoxVec2);
else
// Put the image inside a child window so we can detect when it's fully clipped and doesn't need to be loaded
if (ImGui::BeginChild("img", ImVec2(0, 0), ImGuiChildFlags_AutoResizeX | ImGuiChildFlags_AutoResizeY, ImGuiWindowFlags_None))
{
pressed = ImGui::Button(gameName.c_str(), responsiveBoxVec2);
gameTooltip(game.name);
ImguiTexture tex(art.boxartPath);
pressed = gameImageButton(tex, game.name, responsiveBoxVec2, gameName);
}
ImGui::EndChild();
}
else
{
@ -3381,6 +3393,9 @@ void gui_display_ui()
case GuiState::Cheats:
gui_cheats();
break;
case GuiState::Achievements:
achievements::achievementList();
break;
default:
die("Unknown UI state");
break;

View File

@ -63,7 +63,8 @@ enum class GuiState {
SelectDisk,
Loading,
NetworkStart,
Cheats
Cheats,
Achievements,
};
extern GuiState gui_state;

View File

@ -21,7 +21,10 @@
#include "gui_util.h"
#include "imgui_driver.h"
#include "stdclass.h"
#include "achievements/achievements.h"
#include "IconsFontAwesome6.h"
#include <cmath>
#include <sstream>
extern ImFont *largeFont;
@ -33,6 +36,7 @@ Notification notifier;
static constexpr u64 DISPLAY_TIME = 5000;
static constexpr u64 START_ANIM_TIME = 500;
static constexpr u64 END_ANIM_TIME = 1000;
static constexpr u64 NEVER_ENDS = 1000000000000;
void Notification::notify(Type type, const std::string& image, const std::string& text1,
const std::string& text2, const std::string& text3)
@ -47,7 +51,7 @@ void Notification::notify(Type type, const std::string& image, const std::string
{
// New progress
startTime = now;
endTime = 0x1000000000000; // never
endTime = NEVER_ENDS;
}
}
else
@ -61,23 +65,59 @@ void Notification::notify(Type type, const std::string& image, const std::string
endTime = startTime + DISPLAY_TIME;
}
this->type = type;
this->imagePath = image;
this->imageId = {};
this->image = { image };
text[0] = text1;
text[1] = text2;
text[2] = text3;
}
void Notification::showChallenge(const std::string& image)
{
std::lock_guard<std::mutex> _(mutex);
ImguiTexture texture{ image };
if (std::find(challenges.begin(), challenges.end(), texture) != challenges.end())
return;
challenges.push_back(texture);
if (this->type == None)
{
this->type = Challenge;
startTime = getTimeMs();
endTime = NEVER_ENDS;
}
}
void Notification::hideChallenge(const std::string& image)
{
std::lock_guard<std::mutex> _(mutex);
auto it = std::find(challenges.begin(), challenges.end(), image);
if (it == challenges.end())
return;
challenges.erase(it);
if (this->type == Challenge && challenges.empty())
endTime = getTimeMs();
}
bool Notification::draw()
{
std::lock_guard<std::mutex> _(mutex);
if (type == None)
return false;
u64 now = getTimeMs();
if (now > endTime + END_ANIM_TIME) {
// Hide notification
type = None;
return false;
if (now > endTime + END_ANIM_TIME)
{
if (!challenges.empty())
{
// Show current challenge indicators
type = Challenge;
startTime = getTimeMs();
endTime = NEVER_ENDS;
}
else
{
// Hide notification
type = None;
return false;
}
}
if (now > endTime)
{
@ -89,83 +129,148 @@ bool Notification::draw()
else {
ImGui::SetNextWindowBgAlpha(0.5f);
}
if (imageId == ImTextureID{})
getImage();
float y = ImGui::GetIO().DisplaySize.y;
if (now - startTime < START_ANIM_TIME)
// Slide up
y += 80.f * settings.display.uiScale * (std::cos((now - startTime) / (float)START_ANIM_TIME * (float)M_PI) + 1.f) / 2.f;
ImGui::SetNextWindowPos(ImVec2(0, y), ImGuiCond_Always, ImVec2(0.f, 1.f)); // Lower left corner
ImGui::SetNextWindowSizeConstraints(ScaledVec2(80.f, 80.f) + ImVec2(ImGui::GetStyle().WindowPadding.x * 2, 0.f), ImVec2(FLT_MAX, FLT_MAX));
const float winPaddingX = ImGui::GetStyle().WindowPadding.x;
ImGui::PushStyleVar(ImGuiStyleVar_WindowPadding, ImVec2{});
ImGui::Begin("##achievements", nullptr, ImGuiWindowFlags_AlwaysAutoResize | ImGuiWindowFlags_NoDecoration | ImGuiWindowFlags_NoNav
| ImGuiWindowFlags_NoInputs);
const bool hasPic = imageId != ImTextureID{};
if (ImGui::BeginTable("achievementNotif", hasPic ? 2 : 1, ImGuiTableFlags_SizingFixedFit | ImGuiTableFlags_NoSavedSettings))
if (type == Challenge)
{
if (hasPic)
ImGui::TableSetupColumn("icon", ImGuiTableColumnFlags_WidthFixed);
ImGui::TableSetupColumn("text", ImGuiTableColumnFlags_WidthStretch);
ImGui::TableNextRow();
ImGui::TableSetColumnIndex(0);
if (hasPic)
ImGui::Begin("##achievement", nullptr, ImGuiWindowFlags_AlwaysAutoResize | ImGuiWindowFlags_NoDecoration | ImGuiWindowFlags_NoNav
| ImGuiWindowFlags_NoInputs);
for (const auto& img : challenges)
{
ImGui::Image(imageId, ScaledVec2(80.f, 80.f), { 0.f, 0.f }, { 1.f, 1.f });
ImGui::TableSetColumnIndex(1);
img.draw(ScaledVec2(60.f, 60.f));
ImGui::SameLine();
}
float w = largeFont->CalcTextSizeA(largeFont->FontSize, FLT_MAX, -1.f, text[0].c_str()).x;
w = std::max(w, ImGui::CalcTextSize(text[1].c_str()).x);
w = std::max(w, ImGui::CalcTextSize(text[2].c_str()).x) + winPaddingX * 2;
int lines = (int)!text[0].empty() + (int)!text[1].empty() + (int)!text[2].empty();
ImGui::PushStyleVar(ImGuiStyleVar_WindowPadding, ImVec2{ hasPic ? 0.f : winPaddingX, (3 - lines) * ImGui::GetTextLineHeight() / 2 });
if (ImGui::BeginChild("##text", ImVec2(w, 0), ImGuiChildFlags_AlwaysUseWindowPadding, ImGuiWindowFlags_None))
{
ImGui::PushFont(largeFont);
ImGui::Text("%s", text[0].c_str());
ImGui::PopFont();
if (!text[1].empty())
ImGui::TextColored(ImVec4(1, 1, 0, 0.7f), "%s", text[1].c_str());
if (!text[2].empty())
ImGui::TextColored(ImVec4(1, 1, 0, 0.7f), "%s", text[2].c_str());
}
ImGui::EndChild();
ImGui::PopStyleVar();
ImGui::EndTable();
ImGui::End();
}
else
{
ImGui::SetNextWindowSizeConstraints(ScaledVec2(80.f, 80.f) + ImVec2(ImGui::GetStyle().WindowPadding.x * 2, 0.f), ImVec2(FLT_MAX, FLT_MAX));
const float winPaddingX = ImGui::GetStyle().WindowPadding.x;
ImguiStyleVar _(ImGuiStyleVar_WindowPadding, ImVec2{});
ImGui::Begin("##achievement", nullptr, ImGuiWindowFlags_AlwaysAutoResize | ImGuiWindowFlags_NoDecoration | ImGuiWindowFlags_NoNav
| ImGuiWindowFlags_NoInputs);
ImTextureID imageId = image.getId();
const bool hasPic = imageId != ImTextureID{};
if (ImGui::BeginTable("achievementNotif", hasPic ? 2 : 1, ImGuiTableFlags_SizingFixedFit | ImGuiTableFlags_NoSavedSettings))
{
if (hasPic)
ImGui::TableSetupColumn("icon", ImGuiTableColumnFlags_WidthFixed);
ImGui::TableSetupColumn("text", ImGuiTableColumnFlags_WidthStretch);
ImGui::TableNextRow();
ImGui::TableSetColumnIndex(0);
if (hasPic)
{
image.draw(ScaledVec2(80.f, 80.f));
ImGui::TableSetColumnIndex(1);
}
float w = largeFont->CalcTextSizeA(largeFont->FontSize, FLT_MAX, -1.f, text[0].c_str()).x;
w = std::max(w, ImGui::CalcTextSize(text[1].c_str()).x);
w = std::max(w, ImGui::CalcTextSize(text[2].c_str()).x) + winPaddingX * 2;
int lines = (int)!text[0].empty() + (int)!text[1].empty() + (int)!text[2].empty();
ImguiStyleVar _(ImGuiStyleVar_WindowPadding, ImVec2{ hasPic ? 0.f : winPaddingX, (3 - lines) * ImGui::GetTextLineHeight() / 2 });
if (ImGui::BeginChild("##text", ImVec2(w, 0), ImGuiChildFlags_AlwaysUseWindowPadding, ImGuiWindowFlags_None))
{
ImGui::PushFont(largeFont);
ImGui::Text("%s", text[0].c_str());
ImGui::PopFont();
if (!text[1].empty())
ImGui::TextColored(ImVec4(1, 1, 0, 0.7f), "%s", text[1].c_str());
if (!text[2].empty())
ImGui::TextColored(ImVec4(1, 1, 0, 0.7f), "%s", text[2].c_str());
}
ImGui::EndChild();
ImGui::EndTable();
}
ImGui::End();
}
ImGui::End();
ImGui::PopStyleVar();
ImGui::GetStyle().Alpha = 1.f;
return true;
}
void Notification::getImage()
void achievementList()
{
if (imagePath.empty())
return;
fullScreenWindow(false);
ImguiStyleVar _(ImGuiStyleVar_WindowBorderSize, 0);
ImGui::Begin("##achievements", nullptr, ImGuiWindowFlags_NoDecoration | ImGuiWindowFlags_AlwaysAutoResize);
// Get the texture. Load it if needed.
imageId = imguiDriver->getTexture(imagePath);
if (imageId == ImTextureID())
{
int width, height;
u8 *imgData = loadImage(imagePath, width, height);
if (imgData != nullptr)
float w = ImGui::GetWindowContentRegionMax().x - ImGui::CalcTextSize("Close").x - ImGui::GetStyle().ItemSpacing.x * 2 - ImGui::GetStyle().WindowPadding.x
- (80.f + 20.f * 2) * settings.display.uiScale; // image width and button frame padding
Game game = getCurrentGame();
ImguiTexture tex(game.image);
tex.draw(ScaledVec2(80.f, 80.f));
ImGui::SameLine();
ImGui::BeginChild("game_info", ImVec2(w, 80.f * settings.display.uiScale), ImGuiChildFlags_None, ImGuiWindowFlags_None);
ImGui::PushFont(largeFont);
ImGui::Text("%s", game.title.c_str());
ImGui::PopFont();
std::stringstream ss;
ss << "You have unlocked " << game.unlockedAchievements << " of " << game.totalAchievements
<< " achievements and " << game.points << " of " << game.totalPoints << " points.";
ImGui::PushStyleColor(ImGuiCol_Text, ImVec4(0.75f, 0.75f, 0.75f, 1.f));
ImGui::TextWrapped("%s", ss.str().c_str());
ImGui::PopStyleColor();
ImGui::EndChild();
ImGui::SameLine();
ImguiStyleVar _(ImGuiStyleVar_FramePadding, ScaledVec2(20, 8));
if (ImGui::Button("Close"))
gui_setState(GuiState::Commands);
}
if (ImGui::BeginChild(ImGui::GetID("ach_list"), ImVec2(0, 0), ImGuiChildFlags_Border, ImGuiWindowFlags_DragScrolling | ImGuiWindowFlags_NavFlattened))
{
std::vector<Achievement> achList = getAchievementList();
int id = 0;
std::string category;
for (const auto& ach : achList)
{
try {
imageId = imguiDriver->updateTextureAndAspectRatio(imagePath, imgData, width, height);
} catch (...) {
// vulkan can throw during resizing
if (ach.category != category)
{
category = ach.category;
ImGui::Indent(10 * settings.display.uiScale);
if (category == "Locked" || category == "Active Challenges" || category == "Almost There")
ImGui::Text(ICON_FA_LOCK);
else if (category == "Unlocked" || category == "Recently Unlocked")
ImGui::Text(ICON_FA_LOCK_OPEN);
ImGui::SameLine();
ImGui::PushFont(largeFont);
ImGui::Text("%s", category.c_str());
ImGui::PopFont();
ImGui::Unindent(10 * settings.display.uiScale);
}
free(imgData);
ImguiID _("achiev" + std::to_string(id++));
ImguiTexture tex(ach.image);
tex.draw(ScaledVec2(80.f, 80.f));
ImGui::SameLine();
ImGui::BeginChild(ImGui::GetID("ach_item"), ImVec2(0, 0), ImGuiChildFlags_AutoResizeY, ImGuiWindowFlags_None);
ImGui::PushFont(largeFont);
ImGui::Text("%s", ach.title.c_str());
ImGui::PopFont();
ImGui::PushStyleColor(ImGuiCol_Text, ImVec4(0.75f, 0.75f, 0.75f, 1.f));
ImGui::TextWrapped("%s", ach.description.c_str());
ImGui::TextWrapped("%s", ach.status.c_str());
ImGui::PopStyleColor();
scrollWhenDraggingOnVoid();
ImGui::EndChild();
}
}
scrollWhenDraggingOnVoid();
windowDragScroll();
ImGui::EndChild();
ImGui::End();
}
} // namespace achievements

View File

@ -18,7 +18,9 @@
*/
#include "types.h"
#include "imgui.h"
#include "gui_util.h"
#include <mutex>
#include <vector>
namespace achievements
{
@ -33,24 +35,27 @@ public:
Unlocked,
Progress,
Mastery,
Challenge,
Error
};
void notify(Type type, const std::string& image, const std::string& text1,
const std::string& text2 = {}, const std::string& text3 = {});
void showChallenge(const std::string& image);
void hideChallenge(const std::string& image);
bool draw();
private:
void getImage();
u64 startTime = 0;
u64 endTime = 0;
Type type = Type::None;
std::string imagePath;
ImTextureID imageId {};
ImguiTexture image;
std::string text[3];
std::mutex mutex;
std::vector<ImguiTexture> challenges;
};
extern Notification notifier;
void achievementList();
}

View File

@ -25,11 +25,9 @@
#include "types.h"
#include "stdclass.h"
#include "oslib/storage.h"
#include "imgui_driver.h"
#include "imgui.h"
#include "imgui_internal.h"
#define STBI_ONLY_JPEG
#define STBI_ONLY_PNG
#include <stb_image.h>
static std::string select_current_directory = "**home**";
static std::vector<hostfs::FileInfo> subfolders;
@ -697,17 +695,58 @@ void windowDragScroll()
}
}
u8 *loadImage(const std::string& path, int& width, int& height)
ImTextureID ImguiTexture::getId() const
{
FILE *file = nowide::fopen(path.c_str(), "rb");
if (file == nullptr)
return nullptr;
if (path.empty())
return {};
return imguiDriver->getOrLoadTexture(path);
}
int channels;
stbi_set_flip_vertically_on_load(0);
u8 *imgData = stbi_load_from_file(file, &width, &height, &channels, STBI_rgb_alpha);
std::fclose(file);
return imgData;
static void setUV(float ar, ImVec2& uv0, ImVec2& uv1)
{
uv0 = { 0.f, 0.f };
uv1 = { 1.f, 1.f };
if (ar > 1)
{
uv0.y = -(ar - 1) / 2;
uv1.y = 1 + (ar - 1) / 2;
}
else if (ar != 0)
{
ar = 1 / ar;
uv0.x = -(ar - 1) / 2;
uv1.x = 1 + (ar - 1) / 2;
}
}
void ImguiTexture::draw(const ImVec2& size, const ImVec4& tint_col, const ImVec4& border_col) const
{
ImTextureID id = getId();
if (id == ImTextureID{})
ImGui::Dummy(size);
else
{
float ar = imguiDriver->getAspectRatio(id);
ImVec2 uv0, uv1;
setUV(ar, uv0, uv1);
ImGui::Image(id, size, uv0, uv1, tint_col, border_col);
}
}
bool ImguiTexture::button(const char* str_id, const ImVec2& image_size, const std::string& title,
const ImVec4& bg_col, const ImVec4& tint_col) const
{
ImTextureID id = getId();
if (id == ImTextureID{})
return ImGui::Button(title.c_str(), image_size);
else
{
float ar = imguiDriver->getAspectRatio(id);
ImVec2 uv0, uv1;
setUV(ar, uv0, uv1);
ImVec2 size = image_size - ImGui::GetStyle().FramePadding * 2;
return ImGui::ImageButton(str_id, id, size, uv0, uv1, bg_col, tint_col);
}
}
// Custom version of ImGui::BeginListBox that allows passing window flags

View File

@ -59,16 +59,6 @@ static inline void centerNextWindow()
ImGuiCond_Always, ImVec2(0.5f, 0.5f));
}
static inline bool operator==(const ImVec2& l, const ImVec2& r)
{
return l.x == r.x && l.y == r.y;
}
static inline bool operator!=(const ImVec2& l, const ImVec2& r)
{
return !(l == r);
}
void fullScreenWindow(bool modal);
void windowDragScroll();
@ -130,20 +120,6 @@ struct ScaledVec2 : public ImVec2
inline static ImVec2 min(const ImVec2& l, const ImVec2& r) {
return ImVec2(std::min(l.x, r.x), std::min(l.y, r.y));
}
inline static ImVec2 operator+(const ImVec2& l, const ImVec2& r) {
return ImVec2(l.x + r.x, l.y + r.y);
}
inline static ImVec2 operator-(const ImVec2& l, const ImVec2& r) {
return ImVec2(l.x - r.x, l.y - r.y);
}
inline static ImVec2 operator*(const ImVec2& v, float f) {
return ImVec2(v.x * f, v.y * f);
}
inline static ImVec2 operator/(const ImVec2& v, float f) {
return ImVec2(v.x / f, v.y / f);
}
u8 *loadImage(const std::string& path, int& width, int& height);
class DisabledScope
{
@ -173,3 +149,69 @@ private:
};
bool BeginListBox(const char* label, const ImVec2& size_arg = ImVec2(0, 0), ImGuiWindowFlags windowFlags = 0);
class ImguiID
{
public:
ImguiID(const std::string& id)
: ImguiID(id.c_str()) {}
ImguiID(const char *id) {
ImGui::PushID(id);
}
~ImguiID() {
ImGui::PopID();
}
};
class ImguiStyleVar
{
public:
ImguiStyleVar(ImGuiStyleVar idx, const ImVec2& val) {
ImGui::PushStyleVar(idx, val);
}
ImguiStyleVar(ImGuiStyleVar idx, float val) {
ImGui::PushStyleVar(idx, val);
}
~ImguiStyleVar() {
ImGui::PopStyleVar();
}
};
class ImguiStyleColor
{
public:
ImguiStyleColor(ImGuiCol idx, const ImVec4& col) {
ImGui::PushStyleColor(idx, col);
}
ImguiStyleColor(ImGuiCol idx, ImU32 col) {
ImGui::PushStyleColor(idx, col);
}
~ImguiStyleColor() {
ImGui::PopStyleColor();
}
};
class ImguiTexture
{
public:
ImguiTexture() = default;
ImguiTexture(const std::string& path) : path(path) {}
void draw(const ImVec2& size, const ImVec4& tint_col = ImVec4(1, 1, 1, 1),
const ImVec4& border_col = ImVec4(0, 0, 0, 0)) const;
bool button(const char* str_id, const ImVec2& image_size, const std::string& title = {}, const ImVec4& bg_col = ImVec4(0, 0, 0, 0),
const ImVec4& tint_col = ImVec4(1, 1, 1, 1)) const;
ImTextureID getId() const;
operator ImTextureID() {
return getId();
}
bool operator==(const ImguiTexture& other) const {
return other.path == path;
}
private:
std::string path;
};

View File

@ -0,0 +1,56 @@
/*
Copyright 2024 flyinghead
This file is part of Flycast.
Flycast 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 2 of the License, or
(at your option) any later version.
Flycast 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 Flycast. If not, see <https://www.gnu.org/licenses/>.
*/
#include "imgui_driver.h"
#define STBI_ONLY_JPEG
#define STBI_ONLY_PNG
#include <stb_image.h>
static u8 *loadImage(const std::string& path, int& width, int& height)
{
FILE *file = nowide::fopen(path.c_str(), "rb");
if (file == nullptr)
return nullptr;
int channels;
stbi_set_flip_vertically_on_load(0);
u8 *imgData = stbi_load_from_file(file, &width, &height, &channels, STBI_rgb_alpha);
std::fclose(file);
return imgData;
}
ImTextureID ImGuiDriver::getOrLoadTexture(const std::string& path)
{
ImTextureID id = getTexture(path);
if (id == ImTextureID() && textureLoadCount < 10)
{
textureLoadCount++;
int width, height;
u8 *imgData = loadImage(path, width, height);
if (imgData != nullptr)
{
try {
id = updateTextureAndAspectRatio(path, imgData, width, height);
} catch (...) {
// vulkan can throw during resizing
}
free(imgData);
}
}
return id;
}

View File

@ -36,20 +36,13 @@ public:
virtual void displayVmus() {}
virtual void displayCrosshairs() {}
virtual void present() = 0;
virtual void setFrameRendered() {}
virtual ImTextureID getTexture(const std::string& name) = 0;
virtual ImTextureID updateTexture(const std::string& name, const u8 *data, int width, int height) = 0;
ImTextureID updateTextureAndAspectRatio(const std::string& name, const u8 *data, int width, int height)
{
ImTextureID textureId = updateTexture(name, data, width, height);
if (textureId != ImTextureID())
aspectRatios[textureId] = (float)width / height;
return textureId;
void doPresent() {
textureLoadCount = 0;
present();
}
virtual void setFrameRendered() {}
float getAspectRatio(ImTextureID textureId) {
auto it = aspectRatios.find(textureId);
if (it != aspectRatios.end())
@ -58,8 +51,25 @@ public:
return 1;
}
ImTextureID getOrLoadTexture(const std::string& path);
protected:
virtual ImTextureID getTexture(const std::string& name) = 0;
virtual ImTextureID updateTexture(const std::string& name, const u8 *data, int width, int height) = 0;
virtual void present() = 0;
private:
ImTextureID updateTextureAndAspectRatio(const std::string& name, const u8 *data, int width, int height)
{
textureLoadCount++;
ImTextureID textureId = updateTexture(name, data, width, height);
if (textureId != ImTextureID())
aspectRatios[textureId] = (float)width / height;
return textureId;
}
std::unordered_map<ImTextureID, float> aspectRatios;
int textureLoadCount = 0;
};
extern std::unique_ptr<ImGuiDriver> imguiDriver;

View File

@ -96,7 +96,7 @@ void mainui_loop()
if (imguiDriver == nullptr)
forceReinit = true;
else
imguiDriver->present();
imguiDriver->doPresent();
if (config::RendererType != currentRenderer || forceReinit)
{

BIN
fonts/fa-solid-900.ttf.zip Normal file

Binary file not shown.