From 7e1fe166eeeb7011916749fcf91717c1e39e8734 Mon Sep 17 00:00:00 2001 From: Connor McLaughlin Date: Sun, 21 Feb 2021 16:55:28 +1000 Subject: [PATCH] FullscreenUI: Add notifications API --- src/frontend-common/fullscreen_ui.cpp | 93 ++++++++++---- src/frontend-common/fullscreen_ui.h | 3 + src/frontend-common/imgui_fullscreen.cpp | 156 ++++++++++++++++++++--- src/frontend-common/imgui_fullscreen.h | 9 +- 4 files changed, 220 insertions(+), 41 deletions(-) diff --git a/src/frontend-common/fullscreen_ui.cpp b/src/frontend-common/fullscreen_ui.cpp index 48823f005..385e6df18 100644 --- a/src/frontend-common/fullscreen_ui.cpp +++ b/src/frontend-common/fullscreen_ui.cpp @@ -5,6 +5,7 @@ #include "common/byte_stream.h" #include "common/file_system.h" #include "common/log.h" +#include "common/lru_cache.h" #include "common/make_array.h" #include "common/string.h" #include "common/string_util.h" @@ -99,15 +100,19 @@ static std::unique_ptr LoadTextureResource(const char* name) static bool LoadResources(); static void DestroyResources(); -std::unique_ptr s_app_icon_texture; -std::unique_ptr s_placeholder_texture; -std::array, static_cast(DiscRegion::Count)> s_disc_region_textures; -std::array, static_cast(GameListCompatibilityRating::Count)> +static HostDisplayTexture* GetCachedTexture(const std::string& name); +static ImTextureID ResolveTextureHandle(const std::string& name); + +static std::unique_ptr s_app_icon_texture; +static std::unique_ptr s_placeholder_texture; +static std::array, static_cast(DiscRegion::Count)> s_disc_region_textures; +static std::array, static_cast(GameListCompatibilityRating::Count)> s_game_compatibility_textures; -std::unique_ptr s_fallback_disc_texture; -std::unique_ptr s_fallback_exe_texture; -std::unique_ptr s_fallback_psf_texture; -std::unique_ptr s_fallback_playlist_texture; +static std::unique_ptr s_fallback_disc_texture; +static std::unique_ptr s_fallback_exe_texture; +static std::unique_ptr s_fallback_psf_texture; +static std::unique_ptr s_fallback_playlist_texture; +static LRUCache> s_texture_cache; ////////////////////////////////////////////////////////////////////////// // Settings @@ -195,6 +200,7 @@ bool Initialize(CommonHostInterface* host_interface, SettingsInterface* settings ImGuiFullscreen::UpdateLayoutScale(); ImGuiFullscreen::UpdateFonts(); + ImGuiFullscreen::SetResolveTextureFunction(ResolveTextureHandle); return true; } @@ -388,6 +394,7 @@ bool LoadResources() void DestroyResources() { + s_texture_cache.Clear(); s_app_icon_texture.reset(); s_placeholder_texture.reset(); s_fallback_playlist_texture.reset(); @@ -400,33 +407,46 @@ void DestroyResources() tex.reset(); } -std::unique_ptr LoadTextureResource(const char* name) +static std::unique_ptr LoadTexture(const char* path, bool from_package) { - std::unique_ptr texture; - - const std::string path(StringUtil::StdStringFromFormat("resources" FS_OSPATH_SEPARATOR_STR "%s", name)); - std::unique_ptr stream = s_host_interface->OpenPackageFile(path.c_str(), BYTESTREAM_OPEN_READ); + std::unique_ptr stream; + if (from_package) + stream = s_host_interface->OpenPackageFile(path, BYTESTREAM_OPEN_READ); + else + stream = FileSystem::OpenFile(path, BYTESTREAM_OPEN_READ); if (!stream) { - Log_ErrorPrintf("Failed to open texture resource '%s'", path.c_str()); + Log_ErrorPrintf("Failed to open texture resource '%s'", path); return {}; } Common::RGBA8Image image; - if (Common::LoadImageFromStream(&image, stream.get()) && image.IsValid()) + if (!Common::LoadImageFromStream(&image, stream.get()) && image.IsValid()) { - texture = s_host_interface->GetDisplay()->CreateTexture(image.GetWidth(), image.GetHeight(), 1, 1, 1, - HostDisplayPixelFormat::RGBA8, image.GetPixels(), - image.GetByteStride()); - if (texture) - { - Log_DevPrintf("Uploaded texture resource '%s' (%ux%u)", name, image.GetWidth(), image.GetHeight()); - return texture; - } - - Log_ErrorPrintf("failed to create %ux%u texture for resource", image.GetWidth(), image.GetHeight()); + Log_ErrorPrintf("Failed to read texture resource '%s'", path); + return {}; } + std::unique_ptr texture = s_host_interface->GetDisplay()->CreateTexture( + image.GetWidth(), image.GetHeight(), 1, 1, 1, HostDisplayPixelFormat::RGBA8, image.GetPixels(), + image.GetByteStride()); + if (!texture) + { + Log_ErrorPrintf("failed to create %ux%u texture for resource", image.GetWidth(), image.GetHeight()); + return {}; + } + + Log_DevPrintf("Uploaded texture resource '%s' (%ux%u)", path, image.GetWidth(), image.GetHeight()); + return texture; +} + +std::unique_ptr LoadTextureResource(const char* name) +{ + const std::string path(StringUtil::StdStringFromFormat("resources" FS_OSPATH_SEPARATOR_STR "%s", name)); + std::unique_ptr texture = LoadTexture(path.c_str(), true); + if (texture) + return texture; + Log_ErrorPrintf("Missing resource '%s', using fallback", name); texture = s_host_interface->GetDisplay()->CreateTexture(PLACEHOLDER_ICON_WIDTH, PLACEHOLDER_ICON_HEIGHT, 1, 1, 1, @@ -438,6 +458,29 @@ std::unique_ptr LoadTextureResource(const char* name) return texture; } +HostDisplayTexture* GetCachedTexture(const std::string& name) +{ + std::unique_ptr* tex_ptr = s_texture_cache.Lookup(name); + if (!tex_ptr) + { + std::unique_ptr tex = LoadTexture(name.c_str(), false); + tex_ptr = s_texture_cache.Insert(name, std::move(tex)); + } + + return tex_ptr->get(); +} + +ImTextureID ResolveTextureHandle(const std::string& name) +{ + HostDisplayTexture* tex = GetCachedTexture(name); + return tex ? tex->GetHandle() : nullptr; +} + +bool InvalidateCachedTexture(const std::string& path) +{ + return s_texture_cache.Remove(path); +} + ////////////////////////////////////////////////////////////////////////// // Utility ////////////////////////////////////////////////////////////////////////// diff --git a/src/frontend-common/fullscreen_ui.h b/src/frontend-common/fullscreen_ui.h index b2b6e2668..7cbc0f62c 100644 --- a/src/frontend-common/fullscreen_ui.h +++ b/src/frontend-common/fullscreen_ui.h @@ -1,5 +1,6 @@ #pragma once #include "common/types.h" +#include class CommonHostInterface; class SettingsInterface; @@ -47,6 +48,8 @@ void CloseQuickMenu(); void Shutdown(); void Render(); +bool InvalidateCachedTexture(const std::string& path); + // Returns true if the message has been dismissed. bool DrawErrorWindow(const char* message); bool DrawConfirmWindow(const char* message, bool* result); diff --git a/src/frontend-common/imgui_fullscreen.cpp b/src/frontend-common/imgui_fullscreen.cpp index 1c81ba608..dc2922739 100644 --- a/src/frontend-common/imgui_fullscreen.cpp +++ b/src/frontend-common/imgui_fullscreen.cpp @@ -6,6 +6,7 @@ #include "common/file_system.h" #include "common/string.h" #include "common/string_util.h" +#include "common/timer.h" #include "core/host_display.h" #include "core/host_interface.h" #include "imgui_internal.h" @@ -16,7 +17,8 @@ namespace ImGuiFullscreen { static void DrawFileSelector(); static void DrawChoiceDialog(); -static void DrawBackgroundProgressDialogs(); +static void DrawBackgroundProgressDialogs(ImVec2& position, float spacing); +static void DrawNotifications(ImVec2& position, float spacing); ImFont* g_standard_font = nullptr; ImFont* g_medium_font = nullptr; @@ -33,6 +35,7 @@ static std::string s_icon_font_filename; static std::vector s_icon_font_data; static float s_font_size = 15.0f; static const ImWchar* s_font_glyph_range = nullptr; +static ResolveTextureHandleCallback s_resolve_texture_handle = nullptr; static u32 s_menu_button_index = 0; @@ -75,6 +78,11 @@ void SetMenuBarSize(float size) s_menu_bar_size = size; } +void SetResolveTextureFunction(ResolveTextureHandleCallback callback) +{ + s_resolve_texture_handle = callback; +} + static void AddIconFonts(float size) { static const ImWchar range_fa[] = {ICON_MIN_FA, ICON_MAX_FA, 0}; @@ -182,7 +190,12 @@ void EndLayout() { DrawFileSelector(); DrawChoiceDialog(); - DrawBackgroundProgressDialogs(); + + const float notification_margin = LayoutScale(10.0f); + const float spacing = LayoutScale(10.0f); + ImVec2 position(notification_margin, g_layout_padding_top + LayoutScale(LAYOUT_SCREEN_HEIGHT) - notification_margin); + DrawBackgroundProgressDialogs(position, spacing); + DrawNotifications(position, spacing); ImGui::PopStyleColor(5); ImGui::PopStyleVar(2); @@ -321,7 +334,7 @@ static void GetMenuButtonFrameBounds(float height, ImVec2* pos, ImVec2* size) } static bool MenuButtonFrame(const char* str_id, bool enabled, float height, bool* visible, bool* hovered, ImRect* bb, - ImGuiButtonFlags flags = 0) + ImGuiButtonFlags flags = 0, float hover_alpha = 1.0f) { ImGuiWindow* window = ImGui::GetCurrentWindow(); if (window->SkipItems) @@ -365,7 +378,7 @@ static bool MenuButtonFrame(const char* str_id, bool enabled, float height, bool pressed = ImGui::ButtonBehavior(*bb, id, hovered, &held, flags); if (*hovered) { - const ImU32 col = ImGui::GetColorU32(held ? ImGuiCol_ButtonActive : ImGuiCol_ButtonHovered); + const ImU32 col = ImGui::GetColorU32(held ? ImGuiCol_ButtonActive : ImGuiCol_ButtonHovered, hover_alpha); ImGui::RenderFrame(bb->Min, bb->Max, col, true, 0.0f); } } @@ -383,10 +396,10 @@ static bool MenuButtonFrame(const char* str_id, bool enabled, float height, bool } bool MenuButtonFrame(const char* str_id, bool enabled, float height, bool* visible, bool* hovered, ImVec2* min, - ImVec2* max, ImGuiButtonFlags flags /*= 0*/) + ImVec2* max, ImGuiButtonFlags flags /*= 0*/, float hover_alpha /*= 0*/) { ImRect bb; - const bool result = MenuButtonFrame(str_id, enabled, height, visible, hovered, &bb, flags); + const bool result = MenuButtonFrame(str_id, enabled, height, visible, hovered, &bb, flags, hover_alpha); *min = bb.Min; *max = bb.Max; return result; @@ -1214,7 +1227,7 @@ void CloseBackgroundProgressDialog(const char* str_id) Panic("Closing unknown progress entry."); } -void DrawBackgroundProgressDialogs() +void DrawBackgroundProgressDialogs(ImVec2& position, float spacing) { std::unique_lock lock(s_background_progress_lock); if (s_background_progress_dialogs.empty()) @@ -1222,10 +1235,6 @@ void DrawBackgroundProgressDialogs() const float window_width = LayoutScale(500.0f); const float window_height = LayoutScale(75.0f); - const float window_spacing = LayoutScale(20.0f); - - float current_pos_x = LayoutScale(10.0f); - float current_pos_y = (LayoutScale(LAYOUT_SCREEN_HEIGHT) + g_layout_padding_top) - window_height - LayoutScale(10.0f); ImGui::PushStyleColor(ImGuiCol_WindowBg, UIPrimaryDarkColor()); ImGui::PushStyleColor(ImGuiCol_PlotHistogram, UISecondaryLightColor()); @@ -1239,11 +1248,14 @@ void DrawBackgroundProgressDialogs() for (const BackgroundProgressDialogData& data : s_background_progress_dialogs) { - dl->AddRectFilled(ImVec2(current_pos_x, current_pos_y), - ImVec2(current_pos_x + window_width, current_pos_y + window_height), + const float window_pos_x = position.x; + const float window_pos_y = position.y - window_height; + + dl->AddRectFilled(ImVec2(window_pos_x, window_pos_y), + ImVec2(window_pos_x + window_width, window_pos_y + window_height), IM_COL32(0x11, 0x11, 0x11, 200), LayoutScale(10.0f)); - ImVec2 pos(current_pos_x + LayoutScale(10.0f), current_pos_y + LayoutScale(10.0f)); + ImVec2 pos(window_pos_x + LayoutScale(10.0f), window_pos_y + LayoutScale(10.0f)); dl->AddText(g_medium_font, g_medium_font->FontSize, pos, IM_COL32(255, 255, 255, 255), data.message.c_str(), nullptr, 0.0f); pos.y += g_medium_font->FontSize + LayoutScale(10.0f); @@ -1261,7 +1273,7 @@ void DrawBackgroundProgressDialogs() pos.y + ((bar_end.y - pos.y) / 2.0f) - (text_size.y / 2.0f)); dl->AddText(g_medium_font, g_medium_font->FontSize, text_pos, ImGui::GetColorU32(UIPrimaryTextColor()), text); - current_pos_y -= window_height - window_spacing; + position.y -= window_height + spacing; } ImGui::PopFont(); @@ -1269,4 +1281,118 @@ void DrawBackgroundProgressDialogs() ImGui::PopStyleColor(2); } +////////////////////////////////////////////////////////////////////////// +// Notifications +////////////////////////////////////////////////////////////////////////// +struct Notification +{ + std::string title; + std::string text; + std::string badge_path; + Common::Timer::Value start_time; + float duration; +}; + +static std::vector s_notifications; + +void AddNotification(float duration, std::string title, std::string text, std::string image_path) +{ + Notification notif; + notif.duration = duration; + notif.title = std::move(title); + notif.text = std::move(text); + notif.badge_path = std::move(image_path); + notif.start_time = Common::Timer::GetValue(); + s_notifications.push_back(std::move(notif)); +} + +void DrawNotifications(ImVec2& position, float spacing) +{ + if (s_notifications.empty()) + return; + + const Common::Timer::Value current_time = Common::Timer::GetValue(); + + const float horizontal_padding = ImGuiFullscreen::LayoutScale(20.0f); + const float vertical_padding = ImGuiFullscreen::LayoutScale(10.0f); + const float horizontal_spacing = ImGuiFullscreen::LayoutScale(10.0f); + const float vertical_spacing = ImGuiFullscreen::LayoutScale(4.0f); + const float badge_size = ImGuiFullscreen::LayoutScale(48.0f); + const float min_width = ImGuiFullscreen::LayoutScale(200.0f); + const float max_width = ImGuiFullscreen::LayoutScale(800.0f); + const float max_text_width = max_width - badge_size - (horizontal_padding * 2.0f) - horizontal_spacing; + const float min_height = (vertical_padding * 2.0f) + badge_size; + const float shadow_size = ImGuiFullscreen::LayoutScale(4.0f); + const float rounding = ImGuiFullscreen::LayoutScale(4.0f); + + ImFont* const title_font = ImGuiFullscreen::g_large_font; + ImFont* const text_font = ImGuiFullscreen::g_medium_font; + +#if 0 + static constexpr u32 toast_background_color = IM_COL32(241, 241, 241, 255); + static constexpr u32 toast_border_color = IM_COL32(0x88, 0x88, 0x88, 255); + static constexpr u32 toast_title_color = IM_COL32(1, 1, 1, 255); + static constexpr u32 toast_text_color = IM_COL32(0, 0, 0, 255); +#else + static constexpr u32 toast_background_color = IM_COL32(0x21, 0x21, 0x21, 255); + static constexpr u32 toast_border_color = IM_COL32(0x48, 0x48, 0x48, 255); + static constexpr u32 toast_title_color = IM_COL32(0xff, 0xff, 0xff, 255); + static constexpr u32 toast_text_color = IM_COL32(0xff, 0xff, 0xff, 255); +#endif + + for (u32 index = 0; index < static_cast(s_notifications.size());) + { + Notification& notif = s_notifications[index]; + if (Common::Timer::ConvertValueToSeconds(current_time - notif.start_time) >= notif.duration) + { + s_notifications.erase(s_notifications.begin() + index); + continue; + } + + const ImVec2 title_size(text_font->CalcTextSizeA(title_font->FontSize, max_text_width, max_text_width, + notif.title.c_str(), notif.title.c_str() + notif.title.size())); + + const ImVec2 text_size(text_font->CalcTextSizeA(text_font->FontSize, max_text_width, max_text_width, + notif.text.c_str(), notif.text.c_str() + notif.text.size())); + + const float box_width = std::max( + (horizontal_padding * 2.0f) + badge_size + horizontal_spacing + std::max(title_size.x, text_size.x), min_width); + const float box_height = + std::max((vertical_padding * 2.0f) + title_size.y + vertical_spacing + text_size.y, min_height); + + const ImVec2 box_min(position.x, position.y - box_height); + const ImVec2 box_max(box_min.x + box_width, box_min.y + box_height); + + ImDrawList* dl = ImGui::GetForegroundDrawList(); + dl->AddRectFilled(ImVec2(box_min.x + shadow_size, box_min.y + shadow_size), + ImVec2(box_max.x + shadow_size, box_max.y + shadow_size), IM_COL32(20, 20, 20, 180), rounding, + ImDrawCornerFlags_All); + dl->AddRectFilled(box_min, box_max, toast_background_color, rounding, ImDrawCornerFlags_All); + dl->AddRect(box_min, box_max, toast_border_color, rounding, ImDrawCornerFlags_All, + ImGuiFullscreen::LayoutScale(1.0f)); + + const ImVec2 badge_min(box_min.x + horizontal_padding, box_min.y + vertical_padding); + const ImVec2 badge_max(badge_min.x + badge_size, badge_min.y + badge_size); + if (!notif.badge_path.empty() && s_resolve_texture_handle) + { + ImTextureID tex = s_resolve_texture_handle(notif.badge_path); + if (tex) + dl->AddImage(tex, badge_min, badge_max); + } + + const ImVec2 title_min(badge_max.x + horizontal_spacing, box_min.y + vertical_padding); + const ImVec2 title_max(title_min.x + title_size.x, title_min.y + title_size.y); + dl->AddText(title_font, title_font->FontSize, title_min, toast_title_color, notif.title.c_str(), + notif.title.c_str() + notif.title.size(), max_text_width); + + const ImVec2 text_min(badge_max.x + horizontal_spacing, title_max.y + vertical_spacing); + const ImVec2 text_max(text_min.x + text_size.x, text_min.y + text_size.y); + dl->AddText(text_font, text_font->FontSize, text_min, toast_text_color, notif.text.c_str(), + notif.text.c_str() + notif.text.size(), max_text_width); + + position.y -= box_height + shadow_size + spacing; + index++; + } +} + } // namespace ImGuiFullscreen \ No newline at end of file diff --git a/src/frontend-common/imgui_fullscreen.h b/src/frontend-common/imgui_fullscreen.h index 43178647f..5d2e5a083 100644 --- a/src/frontend-common/imgui_fullscreen.h +++ b/src/frontend-common/imgui_fullscreen.h @@ -10,6 +10,8 @@ namespace ImGuiFullscreen { ImVec4(static_cast((hex >> 16) & 0xFFu) / 255.0f, static_cast((hex >> 8) & 0xFFu) / 255.0f, \ static_cast(hex & 0xFFu) / 255.0f, static_cast(alpha) / 255.0f) +using ResolveTextureHandleCallback = ImTextureID(*)(const std::string& path); + static constexpr float LAYOUT_SCREEN_WIDTH = 1280.0f; static constexpr float LAYOUT_SCREEN_HEIGHT = 720.0f; static constexpr float LAYOUT_LARGE_FONT_SIZE = 26.0f; @@ -133,6 +135,9 @@ void SetFontGlyphRanges(const ImWchar* glyph_ranges); /// Changes the menu bar size. Don't forget to call UpdateLayoutScale() and UpdateFonts(). void SetMenuBarSize(float size); +/// Resolves a texture name to a handle. +void SetResolveTextureFunction(ResolveTextureHandleCallback callback); + /// Rebuilds fonts to a new scale if needed. Returns true if fonts have changed and the texture needs updating. bool UpdateFonts(); @@ -159,7 +164,7 @@ void BeginMenuButtons(u32 num_items = 0, float y_align = 0.0f, float x_padding = float y_padding = LAYOUT_MENU_BUTTON_Y_PADDING, float item_height = LAYOUT_MENU_BUTTON_HEIGHT); void EndMenuButtons(); bool MenuButtonFrame(const char* str_id, bool enabled, float height, bool* visible, bool* hovered, ImVec2* min, - ImVec2* max, ImGuiButtonFlags flags = 0); + ImVec2* max, ImGuiButtonFlags flags = 0, float hover_alpha = 1.0f); void MenuHeading(const char* title, bool draw_line = true); bool ActiveButton(const char* title, bool is_active, bool enabled = true, float height = LAYOUT_MENU_BUTTON_HEIGHT_NO_SUMMARY, ImFont* font = g_large_font); @@ -226,4 +231,6 @@ void OpenBackgroundProgressDialog(const char* str_id, std::string message, s32 m void UpdateBackgroundProgressDialog(const char* str_id, std::string message, s32 min, s32 max, s32 value); void CloseBackgroundProgressDialog(const char* str_id); +void AddNotification(float duration, std::string title, std::string text, std::string image_path); + } // namespace ImGuiFullscreen