FSUI: Use SVG icons for flags and stars

This commit is contained in:
TheLastRar
2025-04-30 22:45:04 +02:00
committed by lightningterror
parent c2af0097c5
commit 9412fbcefb
41 changed files with 266 additions and 11 deletions
Binary file not shown.

Before

Width:  |  Height:  |  Size: 5.4 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.4 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 3.1 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.9 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 4.5 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.4 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 4.9 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 3.9 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 4.1 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 2.7 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 144 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 322 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 2.2 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 261 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 334 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 361 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 655 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 247 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 2.5 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 2.2 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 265 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 675 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 7.2 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 140 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 265 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 4.9 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 587 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 331 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 751 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 2.1 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 296 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 442 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 534 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 505 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 442 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 297 B

+55 -5
View File
@@ -94,6 +94,7 @@ using ImGuiFullscreen::LAYOUT_MENU_BUTTON_X_PADDING;
using ImGuiFullscreen::LAYOUT_MENU_BUTTON_Y_PADDING;
using ImGuiFullscreen::LAYOUT_SCREEN_HEIGHT;
using ImGuiFullscreen::LAYOUT_SCREEN_WIDTH;
using ImGuiFullscreen::SvgScaling;
using ImGuiFullscreen::UIBackgroundColor;
using ImGuiFullscreen::UIBackgroundHighlightColor;
using ImGuiFullscreen::UIBackgroundLineColor;
@@ -131,6 +132,8 @@ using ImGuiFullscreen::EndNavBar;
using ImGuiFullscreen::EnumChoiceButton;
using ImGuiFullscreen::FloatingButton;
using ImGuiFullscreen::ForceKeyNavEnabled;
using ImGuiFullscreen::GetCachedSvgTexture;
using ImGuiFullscreen::GetCachedSvgTextureAsync;
using ImGuiFullscreen::GetCachedTexture;
using ImGuiFullscreen::GetCachedTextureAsync;
using ImGuiFullscreen::GetPlaceholderTexture;
@@ -139,6 +142,7 @@ using ImGuiFullscreen::HorizontalMenuItem;
using ImGuiFullscreen::IsFocusResetQueued;
using ImGuiFullscreen::IsGamepadInputSource;
using ImGuiFullscreen::LayoutScale;
using ImGuiFullscreen::LoadSvgTexture;
using ImGuiFullscreen::LoadTexture;
using ImGuiFullscreen::MenuButton;
using ImGuiFullscreen::MenuButtonFrame;
@@ -237,6 +241,11 @@ namespace FullscreenUI
static void GetStandardSelectionFooterText(SmallStringBase& dest, bool back_instead_of_cancel);
static void ApplyLayoutSettings(const SettingsInterface* bsi = nullptr);
void DrawSvgTexture(GSTexture* padded_texture, ImVec2 unpadded_size);
void DrawSvgTexture_UV(GSTexture* padded_texture, ImVec2 unpadded_size, ImVec2 unpadded_uv0, ImVec2 unpadded_uv1);
void DrawCachedSvgTexture(const std::string& path, ImVec2 size, SvgScaling mode);
void DrawCachedSvgTextureAsync(const std::string& path, ImVec2 size, SvgScaling mode);
static MainWindowType s_current_main_window = MainWindowType::None;
static PauseSubMenu s_current_pause_submenu = PauseSubMenu::None;
static bool s_initialized = false;
@@ -256,6 +265,7 @@ namespace FullscreenUI
// Resources
//////////////////////////////////////////////////////////////////////////
static bool LoadResources();
static bool LoadSvgResources();
static void DestroyResources();
static std::array<std::shared_ptr<GSTexture>, static_cast<u32>(GameDatabaseSchema::Compatibility::Perfect)>
@@ -678,6 +688,34 @@ void FullscreenUI::GamepadLayoutChanged()
ApplyLayoutSettings();
}
// When drawing an svg to a non-integer size, we get a padded texture.
// This function crops off this padding by setting the image UV for the draw.
// We currently only use integer sizes for images, but I wrote this before checking that.
void FullscreenUI::DrawSvgTexture(GSTexture* padded_texture, ImVec2 unpadded_size)
{
if (padded_texture != GetPlaceholderTexture().get())
{
const ImVec2 padded_size(padded_texture->GetWidth(), padded_texture->GetHeight());
const ImVec2 uv1 = unpadded_size / padded_size;
ImGui::Image(reinterpret_cast<ImTextureID>(padded_texture->GetNativeHandle()), unpadded_size, ImVec2(0.0f, 0.0f), uv1);
}
else
{
// Placeholder is a png file and should be scaled by ImGui
ImGui::Image(reinterpret_cast<ImTextureID>(padded_texture->GetNativeHandle()), unpadded_size);
}
}
void FullscreenUI::DrawCachedSvgTexture(const std::string& path, ImVec2 size, SvgScaling mode)
{
DrawSvgTexture(GetCachedSvgTexture(path, size, mode), size);
}
void FullscreenUI::DrawCachedSvgTextureAsync(const std::string& path, ImVec2 size, SvgScaling mode)
{
DrawSvgTexture(GetCachedSvgTextureAsync(path, size, mode), size);
}
//////////////////////////////////////////////////////////////////////////
// Main
//////////////////////////////////////////////////////////////////////////
@@ -727,6 +765,11 @@ bool FullscreenUI::IsInitialized()
return s_initialized;
}
void FullscreenUI::ReloadSvgResources()
{
LoadSvgResources();
}
bool FullscreenUI::HasActiveWindow()
{
return s_initialized && (s_current_main_window != MainWindowType::None || AreAnyDialogsOpen());
@@ -1049,10 +1092,15 @@ bool FullscreenUI::LoadResources()
s_fallback_disc_texture = LoadTexture("fullscreenui/media-cdrom.png");
s_fallback_exe_texture = LoadTexture("fullscreenui/applications-system.png");
return LoadSvgResources();
}
bool FullscreenUI::LoadSvgResources()
{
for (u32 i = static_cast<u32>(GameDatabaseSchema::Compatibility::Nothing);
i <= static_cast<u32>(GameDatabaseSchema::Compatibility::Perfect); i++)
{
s_game_compatibility_textures[i - 1] = LoadTexture(fmt::format("fullscreenui/star-{}.png", i - 1).c_str());
s_game_compatibility_textures[i - 1] = LoadSvgTexture(fmt::format("icons/star-{}.svg", i - 1).c_str(), LayoutScale(64.0f, 16.0f), SvgScaling::Fit);
}
return true;
@@ -6497,10 +6545,13 @@ void FullscreenUI::DrawGameList(const ImVec2& heading_size)
// region
{
std::string flag_texture(fmt::format("fullscreenui/flags/{}.png", GameList::RegionToString(selected_entry->region)));
std::string flag_texture(fmt::format("icons/flags/{}.svg", GameList::RegionToString(selected_entry->region)));
ImGui::TextUnformatted(FSUI_CSTR("Region: "));
ImGui::SameLine();
ImGui::Image(reinterpret_cast<ImTextureID>(GetCachedTextureAsync(flag_texture.c_str())->GetNativeHandle()), LayoutScale(23.0f, 16.0f));
if (selected_entry->region == GameList::Region::Other)
DrawCachedSvgTextureAsync(flag_texture, LayoutScale(23.0f, 16.0f), SvgScaling::Fit);
else
DrawCachedSvgTextureAsync(flag_texture, LayoutScale(23.0f, 16.0f), SvgScaling::ZoomFill);
ImGui::SameLine();
ImGui::Text(" (%s)", GameList::RegionToString(selected_entry->region));
}
@@ -6510,8 +6561,7 @@ void FullscreenUI::DrawGameList(const ImVec2& heading_size)
ImGui::SameLine();
if (selected_entry->compatibility_rating != GameDatabaseSchema::Compatibility::Unknown)
{
ImGui::Image(reinterpret_cast<ImTextureID>(s_game_compatibility_textures[static_cast<u32>(selected_entry->compatibility_rating) - 1]->GetNativeHandle()),
LayoutScale(64.0f, 16.0f));
DrawSvgTexture(s_game_compatibility_textures[static_cast<u32>(selected_entry->compatibility_rating) - 1].get(), LayoutScale(64.0f, 16.0f));
ImGui::SameLine();
}
ImGui::Text(" (%s)", GameList::EntryCompatibilityRatingToString(selected_entry->compatibility_rating));
+1
View File
@@ -17,6 +17,7 @@ namespace FullscreenUI
{
bool Initialize();
bool IsInitialized();
void ReloadSvgResources();
bool HasActiveWindow();
void CheckForConfigChanges(const Pcsx2Config& old_config);
void OnVMStarted();
+196 -4
View File
@@ -26,6 +26,9 @@
#include "imgui_internal.h"
#include "imgui_stdlib.h"
#include <plutovg.h>
#include <plutosvg.h>
#include <array>
#include <cmath>
#include <condition_variable>
@@ -41,6 +44,7 @@ namespace ImGuiFullscreen
static std::optional<RGBA8Image> LoadTextureImage(const char* path);
static std::shared_ptr<GSTexture> UploadTexture(const char* path, const RGBA8Image& image);
static std::optional<RGBA8Image> LoadSvgTextureImage(const char* path, ImVec2 size, SvgScaling mode);
static void TextureLoaderThread();
static void DrawFileSelector();
@@ -88,11 +92,13 @@ namespace ImGuiFullscreen
static FocusResetType s_focus_reset_queued = FocusResetType::None;
static LRUCache<std::string, std::shared_ptr<GSTexture>> s_texture_cache(128, true);
static LRUCache<std::string, std::vector<u8>> s_svg_data_cache(64, true);
static std::shared_ptr<GSTexture> s_placeholder_texture;
static std::atomic_bool s_texture_load_thread_quit{false};
static std::mutex s_texture_load_mutex;
static std::condition_variable s_texture_load_cv;
static std::deque<std::string> s_texture_load_queue;
static std::deque<std::tuple<std::string, ImVec2, SvgScaling>> s_svg_texture_load_queue;
static std::deque<std::pair<std::string, RGBA8Image>> s_texture_upload_queue;
static Threading::Thread s_texture_load_thread;
@@ -367,6 +373,166 @@ GSTexture* ImGuiFullscreen::GetCachedTextureAsync(std::string_view name)
return tex_ptr->get();
}
// Renders an SVG image to specified size into a texture of ceil(size).
// The region donated by size is positioned top left of the image when size != ceil(size).
// The image is scaled to fit within, and rendered center of, size.
std::optional<RGBA8Image> ImGuiFullscreen::LoadSvgTextureImage(const char* path, ImVec2 size, SvgScaling mode)
{
std::optional<RGBA8Image> image;
// Cache SVG files as we may need to re-render at a new size later.
std::vector<u8>* data_ptr = s_svg_data_cache.Lookup(path);
if (!data_ptr)
{
std::optional<std::vector<u8>> data;
if (Path::IsAbsolute(path))
data = FileSystem::ReadBinaryFile(path);
else
data = FileSystem::ReadBinaryFile(Path::Combine(EmuFolders::Resources, path).c_str());
if (data.has_value())
data_ptr = s_svg_data_cache.Insert(std::string(path), std::move(data.value()));
}
if (data_ptr)
{
// Load SVG
plutosvg_document_t* pluto_svg = plutosvg_document_load_from_data(reinterpret_cast<char*>(data_ptr->data()),
data_ptr->size(), -1, -1, nullptr, nullptr);
if (pluto_svg == nullptr)
{
Console.Error("Failed to load svg '%s'", path);
return image;
}
const float base_width = plutosvg_document_get_width(pluto_svg);
const float base_height = plutosvg_document_get_height(pluto_svg);
// Create a surface large enough to store the SVG.
const int px_width = std::ceil(size.x);
const int px_height = std::ceil(size.y);
std::vector<u32> pixel_data;
pixel_data.resize(px_width * px_height);
plutovg_surface_t* pluto_surface = plutovg_surface_create_for_data(reinterpret_cast<unsigned char*>(pixel_data.data()),
px_width, px_height, px_width * 4);
if (pluto_surface == nullptr)
{
plutosvg_document_destroy(pluto_svg);
Console.Error("Failed to create plutovg surface '%s'", path);
return image;
}
// Create a drawing canvas.
plutovg_canvas_t* pluto_canvas = plutovg_canvas_create(pluto_surface);
if (pluto_canvas == nullptr)
{
plutovg_surface_destroy(pluto_surface);
plutosvg_document_destroy(pluto_svg);
Console.Error("Failed to create plutovg canvas '%s'", path);
return image;
}
// Scale & position SVG.
// ImGui positions images from top left, so we can ignore integer size of surface.
switch (mode)
{
case SvgScaling::Stretch:
plutovg_canvas_scale(pluto_canvas, size.x / base_width, size.y / base_height);
break;
case SvgScaling::Fit:
{
const ImRect rect = CenterImage(size, {base_width, base_height}, false);
const float scale = (rect.Max.y - rect.Min.y) / base_height;
plutovg_canvas_scale(pluto_canvas, scale, scale);
plutovg_canvas_translate(pluto_canvas, rect.Min.x / scale, rect.Min.y / scale);
break;
}
case SvgScaling::ZoomFill:
{
const ImRect rect = CenterImage(size, {base_width, base_height}, true);
const float scale = (rect.Max.y - rect.Min.y) / base_height;
plutovg_canvas_scale(pluto_canvas, scale, scale);
plutovg_canvas_translate(pluto_canvas, rect.Min.x / scale, rect.Min.y / scale);
break;
}
}
// Render
const bool success = plutosvg_document_render(pluto_svg, nullptr, pluto_canvas, nullptr, nullptr, nullptr);
// Free pluto objects
plutovg_canvas_destroy(pluto_canvas);
plutovg_surface_destroy(pluto_surface);
plutosvg_document_destroy(pluto_svg);
if (!success)
{
Console.Error("Failed to render svg '%s'", path);
return image;
}
// Convert to RGBA8Image
plutovg_convert_argb_to_rgba(reinterpret_cast<unsigned char*>(pixel_data.data()), reinterpret_cast<unsigned char*>(pixel_data.data()),
px_width, px_height, px_width * 4);
image = RGBA8Image(px_width, px_height, std::move(pixel_data));
}
else
{
Console.Error("Failed to open texture resource '%s'", path);
}
return image;
}
std::shared_ptr<GSTexture> ImGuiFullscreen::LoadSvgTexture(std::string_view path, ImVec2 size, SvgScaling mode)
{
std::string path_str(path);
std::optional<RGBA8Image> image(LoadSvgTextureImage(path_str.c_str(), size, mode));
if (image.has_value())
{
std::shared_ptr<GSTexture> ret(UploadTexture(path_str.c_str(), image.value()));
if (ret)
return ret;
}
return s_placeholder_texture;
}
GSTexture* ImGuiFullscreen::GetCachedSvgTexture(std::string_view name, ImVec2 size, SvgScaling mode)
{
std::string svg_key = fmt::format("{}_{}x{}_{}", name, size.x, size.y, static_cast<u8>(mode));
std::shared_ptr<GSTexture>* tex_ptr = s_texture_cache.Lookup(svg_key);
if (!tex_ptr)
{
std::shared_ptr<GSTexture> tex(LoadSvgTexture(name, size, mode));
tex_ptr = s_texture_cache.Insert(std::string(svg_key), std::move(tex));
}
return tex_ptr->get();
}
GSTexture* ImGuiFullscreen::GetCachedSvgTextureAsync(std::string_view name, ImVec2 size, SvgScaling mode)
{
std::string svg_key = fmt::format("{}_{}x{}_{}", name, size.x, size.y, static_cast<u8>(mode));
std::shared_ptr<GSTexture>* tex_ptr = s_texture_cache.Lookup(svg_key);
if (!tex_ptr)
{
// insert the placeholder
tex_ptr = s_texture_cache.Insert(std::string(svg_key), s_placeholder_texture);
// queue the actual load
std::unique_lock lock(s_texture_load_mutex);
s_svg_texture_load_queue.emplace_back(std::tuple(name, size, mode));
s_texture_load_cv.notify_one();
}
return tex_ptr->get();
}
bool ImGuiFullscreen::InvalidateCachedTexture(const std::string& path)
{
return s_texture_cache.Remove(path);
@@ -398,7 +564,12 @@ void ImGuiFullscreen::TextureLoaderThread()
for (;;)
{
s_texture_load_cv.wait(
lock, []() { return (s_texture_load_thread_quit.load(std::memory_order_acquire) || !s_texture_load_queue.empty()); });
lock, []() {
return (
s_texture_load_thread_quit.load(std::memory_order_acquire) ||
!s_texture_load_queue.empty() ||
!s_svg_texture_load_queue.empty());
});
if (s_texture_load_thread_quit.load(std::memory_order_acquire))
break;
@@ -416,9 +587,30 @@ void ImGuiFullscreen::TextureLoaderThread()
if (image)
s_texture_upload_queue.emplace_back(std::move(path), std::move(image.value()));
}
while (!s_svg_texture_load_queue.empty())
{
std::string path;
ImVec2 size;
SvgScaling mode;
std::tie(path, size, mode) = std::move(s_svg_texture_load_queue.front());
s_svg_texture_load_queue.pop_front();
lock.unlock();
std::optional<RGBA8Image> image(LoadSvgTextureImage(path.c_str(), size, mode));
lock.lock();
// don't bother queuing back if it doesn't exist
if (image)
{
std::string svg_key = fmt::format("{}_{}x{}_{}", path, size.x, size.y, static_cast<u8>(mode));
s_texture_upload_queue.emplace_back(std::move(svg_key), std::move(image.value()));
}
}
}
s_texture_load_queue.clear();
s_svg_texture_load_queue.clear();
}
bool ImGuiFullscreen::UpdateLayoutScale()
@@ -451,13 +643,13 @@ bool ImGuiFullscreen::UpdateLayoutScale()
return g_layout_scale != old_scale;
}
ImRect ImGuiFullscreen::CenterImage(const ImVec2& fit_size, const ImVec2& image_size)
ImRect ImGuiFullscreen::CenterImage(const ImVec2& fit_size, const ImVec2& image_size, bool fill)
{
const float fit_ar = fit_size.x / fit_size.y;
const float image_ar = image_size.x / image_size.y;
ImRect ret;
if (fit_ar > image_ar)
if ((fit_ar > image_ar) ^ fill)
{
// center horizontally
const float width = fit_size.y * image_ar;
@@ -477,7 +669,7 @@ ImRect ImGuiFullscreen::CenterImage(const ImVec2& fit_size, const ImVec2& image_
return ret;
}
ImRect ImGuiFullscreen::CenterImage(const ImRect& fit_rect, const ImVec2& image_size)
ImRect ImGuiFullscreen::CenterImage(const ImRect& fit_rect, const ImVec2& image_size, bool fill)
{
ImRect ret(CenterImage(fit_rect.Max - fit_rect.Min, image_size));
ret.Translate(fit_rect.Min);
+11 -2
View File
@@ -86,8 +86,8 @@ namespace ImGuiFullscreen
}
/// Centers an image within the specified bounds, scaling up or down as needed.
ImRect CenterImage(const ImVec2& fit_size, const ImVec2& image_size);
ImRect CenterImage(const ImRect& fit_rect, const ImVec2& image_size);
ImRect CenterImage(const ImVec2& fit_size, const ImVec2& image_size, bool fill = false);
ImRect CenterImage(const ImRect& fit_rect, const ImVec2& image_size, bool fill = false);
/// Initializes, setting up any state.
bool Initialize(const char* placeholder_image_path);
@@ -104,6 +104,15 @@ namespace ImGuiFullscreen
std::shared_ptr<GSTexture> LoadTexture(std::string_view path);
GSTexture* GetCachedTexture(std::string_view name);
GSTexture* GetCachedTextureAsync(std::string_view name);
enum class SvgScaling : u8
{
Stretch,
Fit,
ZoomFill,
};
std::shared_ptr<GSTexture> LoadSvgTexture(std::string_view path, ImVec2 size, SvgScaling mode = SvgScaling::Stretch);
GSTexture* GetCachedSvgTexture(std::string_view name, ImVec2 size, SvgScaling mode = SvgScaling::Stretch);
GSTexture* GetCachedSvgTextureAsync(std::string_view name, ImVec2 size, SvgScaling mode = SvgScaling::Stretch);
bool InvalidateCachedTexture(const std::string& path);
void UploadAsyncTextures();
+3
View File
@@ -255,6 +255,9 @@ void ImGuiManager::UpdateScale()
if (!g_gs_device->UpdateImGuiFontTexture())
pxFailRel("Failed to recreate font texture after scale+resize");
if (FullscreenUI::IsInitialized())
FullscreenUI::ReloadSvgResources();
}
void ImGuiManager::NewFrame()