duckstation

duckstation, but archived from the revision just before upstream changed it to a proprietary software project, this version is the libre one
git clone https://git.neptards.moe/u3shit/duckstation.git
Log | Files | Refs | README | LICENSE

imgui_fullscreen.cpp (111266B)


      1 // SPDX-FileCopyrightText: 2019-2024 Connor McLaughlin <stenzek@gmail.com>
      2 // SPDX-License-Identifier: (GPL-3.0 OR PolyForm-Strict-1.0.0)
      3 
      4 #include "imgui_fullscreen.h"
      5 #include "gpu_device.h"
      6 #include "image.h"
      7 #include "imgui_animated.h"
      8 #include "imgui_manager.h"
      9 
     10 #include "common/assert.h"
     11 #include "common/easing.h"
     12 #include "common/error.h"
     13 #include "common/file_system.h"
     14 #include "common/log.h"
     15 #include "common/lru_cache.h"
     16 #include "common/path.h"
     17 #include "common/string_util.h"
     18 #include "common/threading.h"
     19 #include "common/timer.h"
     20 
     21 #include "core/host.h"
     22 
     23 #include "fmt/core.h"
     24 
     25 #include "IconsFontAwesome5.h"
     26 #include "imgui_internal.h"
     27 #include "imgui_stdlib.h"
     28 
     29 #include <array>
     30 #include <cmath>
     31 #include <condition_variable>
     32 #include <deque>
     33 #include <mutex>
     34 #include <thread>
     35 #include <utility>
     36 #include <variant>
     37 
     38 Log_SetChannel(ImGuiFullscreen);
     39 
     40 namespace ImGuiFullscreen {
     41 using MessageDialogCallbackVariant = std::variant<InfoMessageDialogCallback, ConfirmMessageDialogCallback>;
     42 
     43 static constexpr float MENU_BACKGROUND_ANIMATION_TIME = 0.5f;
     44 
     45 static std::optional<RGBA8Image> LoadTextureImage(std::string_view path);
     46 static std::shared_ptr<GPUTexture> UploadTexture(std::string_view path, const RGBA8Image& image);
     47 static void TextureLoaderThread();
     48 
     49 static void DrawFileSelector();
     50 static void DrawChoiceDialog();
     51 static void DrawInputDialog();
     52 static void DrawMessageDialog();
     53 static void DrawBackgroundProgressDialogs(ImVec2& position, float spacing);
     54 static void DrawNotifications(ImVec2& position, float spacing);
     55 static void DrawToast();
     56 static bool MenuButtonFrame(const char* str_id, bool enabled, float height, bool* visible, bool* hovered, ImRect* bb,
     57                             ImGuiButtonFlags flags = 0, float hover_alpha = 1.0f);
     58 static void PopulateFileSelectorItems();
     59 static void SetFileSelectorDirectory(std::string dir);
     60 static ImGuiID GetBackgroundProgressID(const char* str_id);
     61 
     62 ImFont* g_standard_font = nullptr;
     63 ImFont* g_medium_font = nullptr;
     64 ImFont* g_large_font = nullptr;
     65 ImFont* g_icon_font = nullptr;
     66 
     67 float g_layout_scale = 1.0f;
     68 float g_rcp_layout_scale = 1.0f;
     69 float g_layout_padding_left = 0.0f;
     70 float g_layout_padding_top = 0.0f;
     71 
     72 ImVec4 UIBackgroundColor;
     73 ImVec4 UIBackgroundTextColor;
     74 ImVec4 UIBackgroundLineColor;
     75 ImVec4 UIBackgroundHighlightColor;
     76 ImVec4 UIPopupBackgroundColor;
     77 ImVec4 UIDisabledColor;
     78 ImVec4 UIPrimaryColor;
     79 ImVec4 UIPrimaryLightColor;
     80 ImVec4 UIPrimaryDarkColor;
     81 ImVec4 UIPrimaryTextColor;
     82 ImVec4 UITextHighlightColor;
     83 ImVec4 UIPrimaryLineColor;
     84 ImVec4 UISecondaryColor;
     85 ImVec4 UISecondaryWeakColor;
     86 ImVec4 UISecondaryStrongColor;
     87 ImVec4 UISecondaryTextColor;
     88 
     89 static u32 s_menu_button_index = 0;
     90 static u32 s_close_button_state = 0;
     91 static FocusResetType s_focus_reset_queued = FocusResetType::None;
     92 static bool s_light_theme = false;
     93 
     94 static LRUCache<std::string, std::shared_ptr<GPUTexture>> s_texture_cache(128, true);
     95 static std::shared_ptr<GPUTexture> s_placeholder_texture;
     96 static std::atomic_bool s_texture_load_thread_quit{false};
     97 static std::mutex s_texture_load_mutex;
     98 static std::condition_variable s_texture_load_cv;
     99 static std::deque<std::string> s_texture_load_queue;
    100 static std::deque<std::pair<std::string, RGBA8Image>> s_texture_upload_queue;
    101 static std::thread s_texture_load_thread;
    102 
    103 static SmallString s_fullscreen_footer_text;
    104 static SmallString s_last_fullscreen_footer_text;
    105 static float s_fullscreen_text_change_time;
    106 
    107 static bool s_choice_dialog_open = false;
    108 static bool s_choice_dialog_checkable = false;
    109 static std::string s_choice_dialog_title;
    110 static ChoiceDialogOptions s_choice_dialog_options;
    111 static ChoiceDialogCallback s_choice_dialog_callback;
    112 static ImGuiID s_enum_choice_button_id = 0;
    113 static s32 s_enum_choice_button_value = 0;
    114 static bool s_enum_choice_button_set = false;
    115 
    116 static bool s_input_dialog_open = false;
    117 static std::string s_input_dialog_title;
    118 static std::string s_input_dialog_message;
    119 static std::string s_input_dialog_caption;
    120 static std::string s_input_dialog_text;
    121 static std::string s_input_dialog_ok_text;
    122 static InputStringDialogCallback s_input_dialog_callback;
    123 
    124 static bool s_message_dialog_open = false;
    125 static std::string s_message_dialog_title;
    126 static std::string s_message_dialog_message;
    127 static std::array<std::string, 3> s_message_dialog_buttons;
    128 static MessageDialogCallbackVariant s_message_dialog_callback;
    129 
    130 static ImAnimatedVec2 s_menu_button_frame_min_animated;
    131 static ImAnimatedVec2 s_menu_button_frame_max_animated;
    132 static bool s_had_hovered_menu_item = false;
    133 static bool s_has_hovered_menu_item = false;
    134 static bool s_rendered_menu_item_border = false;
    135 
    136 namespace {
    137 struct FileSelectorItem
    138 {
    139   FileSelectorItem() = default;
    140   FileSelectorItem(std::string display_name_, std::string full_path_, bool is_file_)
    141     : display_name(std::move(display_name_)), full_path(std::move(full_path_)), is_file(is_file_)
    142   {
    143   }
    144   FileSelectorItem(const FileSelectorItem&) = default;
    145   FileSelectorItem(FileSelectorItem&&) = default;
    146   ~FileSelectorItem() = default;
    147 
    148   FileSelectorItem& operator=(const FileSelectorItem&) = default;
    149   FileSelectorItem& operator=(FileSelectorItem&&) = default;
    150 
    151   std::string display_name;
    152   std::string full_path;
    153   bool is_file;
    154 };
    155 } // namespace
    156 
    157 static bool s_file_selector_open = false;
    158 static bool s_file_selector_directory = false;
    159 static std::string s_file_selector_title;
    160 static ImGuiFullscreen::FileSelectorCallback s_file_selector_callback;
    161 static std::string s_file_selector_current_directory;
    162 static std::vector<std::string> s_file_selector_filters;
    163 static std::vector<FileSelectorItem> s_file_selector_items;
    164 
    165 static constexpr float NOTIFICATION_FADE_IN_TIME = 0.2f;
    166 static constexpr float NOTIFICATION_FADE_OUT_TIME = 0.8f;
    167 
    168 namespace {
    169 struct Notification
    170 {
    171   std::string key;
    172   std::string title;
    173   std::string text;
    174   std::string badge_path;
    175   Common::Timer::Value start_time;
    176   Common::Timer::Value move_time;
    177   float duration;
    178   float target_y;
    179   float last_y;
    180 };
    181 } // namespace
    182 
    183 static std::vector<Notification> s_notifications;
    184 
    185 static std::string s_toast_title;
    186 static std::string s_toast_message;
    187 static Common::Timer::Value s_toast_start_time;
    188 static float s_toast_duration;
    189 
    190 namespace {
    191 struct BackgroundProgressDialogData
    192 {
    193   std::string message;
    194   ImGuiID id;
    195   s32 min;
    196   s32 max;
    197   s32 value;
    198 };
    199 } // namespace
    200 
    201 static std::vector<BackgroundProgressDialogData> s_background_progress_dialogs;
    202 static std::mutex s_background_progress_lock;
    203 } // namespace ImGuiFullscreen
    204 
    205 void ImGuiFullscreen::SetFonts(ImFont* standard_font, ImFont* medium_font, ImFont* large_font)
    206 {
    207   g_standard_font = standard_font;
    208   g_medium_font = medium_font;
    209   g_large_font = large_font;
    210 }
    211 
    212 bool ImGuiFullscreen::Initialize(const char* placeholder_image_path)
    213 {
    214   s_focus_reset_queued = FocusResetType::ViewChanged;
    215   s_close_button_state = 0;
    216 
    217   s_placeholder_texture = LoadTexture(placeholder_image_path);
    218   if (!s_placeholder_texture)
    219   {
    220     ERROR_LOG("Missing placeholder texture '{}', cannot continue", placeholder_image_path);
    221     return false;
    222   }
    223 
    224   s_texture_load_thread_quit.store(false, std::memory_order_release);
    225   s_texture_load_thread = std::thread(TextureLoaderThread);
    226   ResetMenuButtonFrame();
    227   return true;
    228 }
    229 
    230 void ImGuiFullscreen::Shutdown()
    231 {
    232   if (s_texture_load_thread.joinable())
    233   {
    234     {
    235       std::unique_lock lock(s_texture_load_mutex);
    236       s_texture_load_thread_quit.store(true, std::memory_order_release);
    237       s_texture_load_cv.notify_one();
    238     }
    239     s_texture_load_thread.join();
    240   }
    241 
    242   s_texture_upload_queue.clear();
    243   s_placeholder_texture.reset();
    244   g_standard_font = nullptr;
    245   g_medium_font = nullptr;
    246   g_large_font = nullptr;
    247 
    248   s_texture_cache.Clear();
    249 
    250   s_notifications.clear();
    251   s_background_progress_dialogs.clear();
    252   s_fullscreen_footer_text.clear();
    253   s_last_fullscreen_footer_text.clear();
    254   s_fullscreen_text_change_time = 0.0f;
    255   CloseInputDialog();
    256   CloseMessageDialog();
    257   s_choice_dialog_open = false;
    258   s_choice_dialog_checkable = false;
    259   s_choice_dialog_title = {};
    260   s_choice_dialog_options.clear();
    261   s_choice_dialog_callback = {};
    262   s_enum_choice_button_id = 0;
    263   s_enum_choice_button_value = 0;
    264   s_enum_choice_button_set = false;
    265   s_file_selector_open = false;
    266   s_file_selector_directory = false;
    267   s_file_selector_title = {};
    268   s_file_selector_callback = {};
    269   s_file_selector_current_directory = {};
    270   s_file_selector_filters.clear();
    271   s_file_selector_items.clear();
    272   s_message_dialog_open = false;
    273   s_message_dialog_title = {};
    274   s_message_dialog_message = {};
    275   s_message_dialog_buttons = {};
    276   s_message_dialog_callback = {};
    277 }
    278 
    279 const std::shared_ptr<GPUTexture>& ImGuiFullscreen::GetPlaceholderTexture()
    280 {
    281   return s_placeholder_texture;
    282 }
    283 
    284 std::unique_ptr<GPUTexture> ImGuiFullscreen::CreateTextureFromImage(const RGBA8Image& image)
    285 {
    286   std::unique_ptr<GPUTexture> ret =
    287     g_gpu_device->CreateTexture(image.GetWidth(), image.GetHeight(), 1, 1, 1, GPUTexture::Type::Texture,
    288                                 GPUTexture::Format::RGBA8, image.GetPixels(), image.GetPitch());
    289   if (!ret) [[unlikely]]
    290     ERROR_LOG("Failed to upload {}x{} RGBA8Image to GPU", image.GetWidth(), image.GetHeight());
    291   return ret;
    292 }
    293 
    294 std::optional<RGBA8Image> ImGuiFullscreen::LoadTextureImage(std::string_view path)
    295 {
    296   std::optional<RGBA8Image> image;
    297   if (Path::IsAbsolute(path))
    298   {
    299     Error error;
    300     std::string path_str(path);
    301     auto fp = FileSystem::OpenManagedCFile(path_str.c_str(), "rb", &error);
    302     if (fp)
    303     {
    304       image = RGBA8Image();
    305       if (!image->LoadFromFile(path_str.c_str(), fp.get()))
    306       {
    307         ERROR_LOG("Failed to read texture file '{}'", path);
    308         image.reset();
    309       }
    310     }
    311     else
    312     {
    313       ERROR_LOG("Failed to open texture file '{}': {}", path, error.GetDescription());
    314     }
    315   }
    316   else
    317   {
    318     std::optional<DynamicHeapArray<u8>> data = Host::ReadResourceFile(path, true);
    319     if (data.has_value())
    320     {
    321       image = RGBA8Image();
    322       if (!image->LoadFromBuffer(path, data->data(), data->size()))
    323       {
    324         ERROR_LOG("Failed to read texture resource '{}'", path);
    325         image.reset();
    326       }
    327     }
    328     else
    329     {
    330       ERROR_LOG("Failed to open texture resource '{}'", path);
    331     }
    332   }
    333 
    334   return image;
    335 }
    336 
    337 std::shared_ptr<GPUTexture> ImGuiFullscreen::UploadTexture(std::string_view path, const RGBA8Image& image)
    338 {
    339   std::unique_ptr<GPUTexture> texture =
    340     g_gpu_device->FetchTexture(image.GetWidth(), image.GetHeight(), 1, 1, 1, GPUTexture::Type::Texture,
    341                                GPUTexture::Format::RGBA8, image.GetPixels(), image.GetPitch());
    342   if (!texture)
    343   {
    344     ERROR_LOG("failed to create {}x{} texture for resource", image.GetWidth(), image.GetHeight());
    345     return {};
    346   }
    347 
    348   DEV_LOG("Uploaded texture resource '{}' ({}x{})", path, image.GetWidth(), image.GetHeight());
    349   return std::shared_ptr<GPUTexture>(texture.release(), GPUDevice::PooledTextureDeleter());
    350 }
    351 
    352 std::shared_ptr<GPUTexture> ImGuiFullscreen::LoadTexture(std::string_view path)
    353 {
    354   std::optional<RGBA8Image> image(LoadTextureImage(path));
    355   if (image.has_value())
    356   {
    357     std::shared_ptr<GPUTexture> ret(UploadTexture(path, image.value()));
    358     if (ret)
    359       return ret;
    360   }
    361 
    362   return s_placeholder_texture;
    363 }
    364 
    365 GPUTexture* ImGuiFullscreen::GetCachedTexture(std::string_view name)
    366 {
    367   std::shared_ptr<GPUTexture>* tex_ptr = s_texture_cache.Lookup(name);
    368   if (!tex_ptr)
    369   {
    370     std::shared_ptr<GPUTexture> tex(LoadTexture(name));
    371     tex_ptr = s_texture_cache.Insert(std::string(name), std::move(tex));
    372   }
    373 
    374   return tex_ptr->get();
    375 }
    376 
    377 GPUTexture* ImGuiFullscreen::GetCachedTextureAsync(std::string_view name)
    378 {
    379   std::shared_ptr<GPUTexture>* tex_ptr = s_texture_cache.Lookup(name);
    380   if (!tex_ptr)
    381   {
    382     // insert the placeholder
    383     tex_ptr = s_texture_cache.Insert(std::string(name), s_placeholder_texture);
    384 
    385     // queue the actual load
    386     std::unique_lock lock(s_texture_load_mutex);
    387     s_texture_load_queue.emplace_back(name);
    388     s_texture_load_cv.notify_one();
    389   }
    390 
    391   return tex_ptr->get();
    392 }
    393 
    394 bool ImGuiFullscreen::InvalidateCachedTexture(const std::string& path)
    395 {
    396   return s_texture_cache.Remove(path);
    397 }
    398 
    399 void ImGuiFullscreen::UploadAsyncTextures()
    400 {
    401   std::unique_lock lock(s_texture_load_mutex);
    402   while (!s_texture_upload_queue.empty())
    403   {
    404     std::pair<std::string, RGBA8Image> it(std::move(s_texture_upload_queue.front()));
    405     s_texture_upload_queue.pop_front();
    406     lock.unlock();
    407 
    408     std::shared_ptr<GPUTexture> tex = UploadTexture(it.first.c_str(), it.second);
    409     if (tex)
    410       s_texture_cache.Insert(std::move(it.first), std::move(tex));
    411 
    412     lock.lock();
    413   }
    414 }
    415 
    416 void ImGuiFullscreen::TextureLoaderThread()
    417 {
    418   Threading::SetNameOfCurrentThread("ImGuiFullscreen Texture Loader");
    419 
    420   std::unique_lock lock(s_texture_load_mutex);
    421 
    422   for (;;)
    423   {
    424     s_texture_load_cv.wait(lock, []() {
    425       return (s_texture_load_thread_quit.load(std::memory_order_acquire) || !s_texture_load_queue.empty());
    426     });
    427 
    428     if (s_texture_load_thread_quit.load(std::memory_order_acquire))
    429       break;
    430 
    431     while (!s_texture_load_queue.empty())
    432     {
    433       std::string path(std::move(s_texture_load_queue.front()));
    434       s_texture_load_queue.pop_front();
    435 
    436       lock.unlock();
    437       std::optional<RGBA8Image> image(LoadTextureImage(path.c_str()));
    438       lock.lock();
    439 
    440       // don't bother queuing back if it doesn't exist
    441       if (image)
    442         s_texture_upload_queue.emplace_back(std::move(path), std::move(image.value()));
    443     }
    444   }
    445 
    446   s_texture_load_queue.clear();
    447 }
    448 
    449 bool ImGuiFullscreen::UpdateLayoutScale()
    450 {
    451   static constexpr float LAYOUT_RATIO = LAYOUT_SCREEN_WIDTH / LAYOUT_SCREEN_HEIGHT;
    452   const ImGuiIO& io = ImGui::GetIO();
    453 
    454   const float screen_width = io.DisplaySize.x;
    455   const float screen_height = io.DisplaySize.y;
    456   const float screen_ratio = screen_width / screen_height;
    457   const float old_scale = g_layout_scale;
    458 
    459   if (screen_ratio > LAYOUT_RATIO)
    460   {
    461     // screen is wider, use height, pad width
    462     g_layout_scale = std::max(screen_height / LAYOUT_SCREEN_HEIGHT, 0.1f);
    463     g_layout_padding_top = 0.0f;
    464     g_layout_padding_left = (screen_width - (LAYOUT_SCREEN_WIDTH * g_layout_scale)) / 2.0f;
    465   }
    466   else
    467   {
    468     // screen is taller, use width, pad height
    469     g_layout_scale = std::max(screen_width / LAYOUT_SCREEN_WIDTH, 0.1f);
    470     g_layout_padding_top = (screen_height - (LAYOUT_SCREEN_HEIGHT * g_layout_scale)) / 2.0f;
    471     g_layout_padding_left = 0.0f;
    472   }
    473 
    474   g_rcp_layout_scale = 1.0f / g_layout_scale;
    475 
    476   return g_layout_scale != old_scale;
    477 }
    478 
    479 ImRect ImGuiFullscreen::CenterImage(const ImVec2& fit_size, const ImVec2& image_size)
    480 {
    481   const float fit_ar = fit_size.x / fit_size.y;
    482   const float image_ar = image_size.x / image_size.y;
    483 
    484   ImRect ret;
    485   if (fit_ar > image_ar)
    486   {
    487     // center horizontally
    488     const float width = fit_size.y * image_ar;
    489     const float offset = (fit_size.x - width) / 2.0f;
    490     const float height = fit_size.y;
    491     ret = ImRect(ImVec2(offset, 0.0f), ImVec2(offset + width, height));
    492   }
    493   else
    494   {
    495     // center vertically
    496     const float height = fit_size.x / image_ar;
    497     const float offset = (fit_size.y - height) / 2.0f;
    498     const float width = fit_size.x;
    499     ret = ImRect(ImVec2(0.0f, offset), ImVec2(width, offset + height));
    500   }
    501 
    502   return ret;
    503 }
    504 
    505 ImRect ImGuiFullscreen::CenterImage(const ImRect& fit_rect, const ImVec2& image_size)
    506 {
    507   ImRect ret(CenterImage(fit_rect.Max - fit_rect.Min, image_size));
    508   ret.Translate(fit_rect.Min);
    509   return ret;
    510 }
    511 
    512 void ImGuiFullscreen::BeginLayout()
    513 {
    514   // we evict from the texture cache at the start of the frame, in case we go over mid-frame,
    515   // we need to keep all those textures alive until the end of the frame
    516   s_texture_cache.ManualEvict();
    517   PushResetLayout();
    518 }
    519 
    520 void ImGuiFullscreen::EndLayout()
    521 {
    522   DrawFileSelector();
    523   DrawChoiceDialog();
    524   DrawInputDialog();
    525   DrawMessageDialog();
    526 
    527   DrawFullscreenFooter();
    528 
    529   const float notification_margin = LayoutScale(10.0f);
    530   const float spacing = LayoutScale(10.0f);
    531   const float notification_vertical_pos = GetNotificationVerticalPosition();
    532   ImVec2 position(notification_margin,
    533                   notification_vertical_pos * ImGui::GetIO().DisplaySize.y +
    534                     ((notification_vertical_pos >= 0.5f) ? -notification_margin : notification_margin));
    535   DrawBackgroundProgressDialogs(position, spacing);
    536   DrawNotifications(position, spacing);
    537   DrawToast();
    538 
    539   PopResetLayout();
    540 
    541   s_fullscreen_footer_text.clear();
    542 
    543   s_rendered_menu_item_border = false;
    544   s_had_hovered_menu_item = std::exchange(s_has_hovered_menu_item, false);
    545 }
    546 
    547 void ImGuiFullscreen::PushResetLayout()
    548 {
    549   ImGui::PushStyleVar(ImGuiStyleVar_WindowBorderSize, 0.0f);
    550   ImGui::PushStyleVar(ImGuiStyleVar_WindowRounding, 0.0f);
    551   ImGui::PushStyleVar(ImGuiStyleVar_WindowPadding, LayoutScale(8.0f, 8.0f));
    552   ImGui::PushStyleVar(ImGuiStyleVar_FramePadding, LayoutScale(4.0f, 3.0f));
    553   ImGui::PushStyleVar(ImGuiStyleVar_ItemSpacing, LayoutScale(8.0f, 4.0f));
    554   ImGui::PushStyleVar(ImGuiStyleVar_ItemInnerSpacing, LayoutScale(4.0f, 4.0f));
    555   ImGui::PushStyleVar(ImGuiStyleVar_CellPadding, LayoutScale(4.0f, 2.0f));
    556   ImGui::PushStyleVar(ImGuiStyleVar_IndentSpacing, LayoutScale(21.0f));
    557   ImGui::PushStyleVar(ImGuiStyleVar_ScrollbarSize, LayoutScale(14.0f));
    558   ImGui::PushStyleVar(ImGuiStyleVar_ScrollbarRounding, 0.0f);
    559   ImGui::PushStyleVar(ImGuiStyleVar_GrabMinSize, LayoutScale(10.0f));
    560   ImGui::PushStyleVar(ImGuiStyleVar_TabRounding, LayoutScale(4.0f));
    561   ImGui::PushStyleColor(ImGuiCol_Text, UISecondaryTextColor);
    562   ImGui::PushStyleColor(ImGuiCol_TextDisabled, UIDisabledColor);
    563   ImGui::PushStyleColor(ImGuiCol_Button, UISecondaryColor);
    564   ImGui::PushStyleColor(ImGuiCol_ButtonActive, UIBackgroundColor);
    565   ImGui::PushStyleColor(ImGuiCol_ButtonHovered, UIBackgroundHighlightColor);
    566   ImGui::PushStyleColor(ImGuiCol_Border, UIBackgroundLineColor);
    567   ImGui::PushStyleColor(ImGuiCol_ScrollbarBg, UIBackgroundColor);
    568   ImGui::PushStyleColor(ImGuiCol_ScrollbarGrab, UIPrimaryColor);
    569   ImGui::PushStyleColor(ImGuiCol_ScrollbarGrabHovered, UIPrimaryLightColor);
    570   ImGui::PushStyleColor(ImGuiCol_ScrollbarGrabActive, UIPrimaryDarkColor);
    571   ImGui::PushStyleColor(ImGuiCol_PopupBg, UIPopupBackgroundColor);
    572 }
    573 
    574 void ImGuiFullscreen::PopResetLayout()
    575 {
    576   ImGui::PopStyleColor(11);
    577   ImGui::PopStyleVar(12);
    578 }
    579 
    580 void ImGuiFullscreen::QueueResetFocus(FocusResetType type)
    581 {
    582   s_focus_reset_queued = type;
    583   s_close_button_state = 0;
    584 }
    585 
    586 bool ImGuiFullscreen::ResetFocusHere()
    587 {
    588   if (s_focus_reset_queued == FocusResetType::None)
    589     return false;
    590 
    591   // don't take focus from dialogs
    592   ImGuiWindow* window = ImGui::GetCurrentWindow();
    593   if (ImGui::FindBlockingModal(window))
    594     return false;
    595 
    596   s_focus_reset_queued = FocusResetType::None;
    597 
    598   // Set the flag that we drew an active/hovered item active for a frame, because otherwise there's one frame where
    599   // there'll be no frame drawn, which will cancel the animation. Also set the appearing flag, so that the default
    600   // focus set does actually go through.
    601   if (!GImGui->NavDisableHighlight && GImGui->NavDisableMouseHover)
    602   {
    603     window->Appearing = true;
    604     s_has_hovered_menu_item = s_had_hovered_menu_item;
    605   }
    606 
    607   ImGui::SetWindowFocus();
    608   ImGui::NavInitWindow(window, true);
    609 
    610   // only do the active selection magic when we're using keyboard/gamepad
    611   return (GImGui->NavInputSource == ImGuiInputSource_Keyboard || GImGui->NavInputSource == ImGuiInputSource_Gamepad);
    612 }
    613 
    614 bool ImGuiFullscreen::IsFocusResetQueued()
    615 {
    616   return (s_focus_reset_queued != FocusResetType::None);
    617 }
    618 
    619 bool ImGuiFullscreen::IsFocusResetFromWindowChange()
    620 {
    621   return (s_focus_reset_queued != FocusResetType::None && s_focus_reset_queued != FocusResetType::PopupClosed);
    622 }
    623 
    624 ImGuiFullscreen::FocusResetType ImGuiFullscreen::GetQueuedFocusResetType()
    625 {
    626   return s_focus_reset_queued;
    627 }
    628 
    629 void ImGuiFullscreen::ForceKeyNavEnabled()
    630 {
    631   ImGuiContext& g = *ImGui::GetCurrentContext();
    632   g.ActiveIdSource = (g.ActiveIdSource == ImGuiInputSource_Mouse) ? ImGuiInputSource_Keyboard : g.ActiveIdSource;
    633   g.NavInputSource = (g.NavInputSource == ImGuiInputSource_Mouse) ? ImGuiInputSource_Keyboard : g.ActiveIdSource;
    634   g.NavDisableHighlight = false;
    635   g.NavDisableMouseHover = true;
    636 }
    637 
    638 bool ImGuiFullscreen::WantsToCloseMenu()
    639 {
    640   ImGuiContext& g = *GImGui;
    641 
    642   // Wait for the Close button to be released, THEN pressed
    643   if (s_close_button_state == 0)
    644   {
    645     if (ImGui::IsKeyPressed(ImGuiKey_Escape, false))
    646       s_close_button_state = 1;
    647     else if (ImGui::IsKeyPressed(ImGuiKey_NavGamepadCancel, false))
    648       s_close_button_state = 2;
    649   }
    650   else if ((s_close_button_state == 1 && ImGui::IsKeyReleased(ImGuiKey_Escape)) ||
    651            (s_close_button_state == 2 && ImGui::IsKeyReleased(ImGuiKey_NavGamepadCancel)))
    652   {
    653     s_close_button_state = 3;
    654   }
    655   return s_close_button_state > 1;
    656 }
    657 
    658 void ImGuiFullscreen::ResetCloseMenuIfNeeded()
    659 {
    660   // If s_close_button_state reached the "Released" state, reset it after the tick
    661   if (s_close_button_state > 1)
    662   {
    663     s_close_button_state = 0;
    664   }
    665 }
    666 
    667 void ImGuiFullscreen::PushPrimaryColor()
    668 {
    669   ImGui::PushStyleColor(ImGuiCol_Text, UIPrimaryTextColor);
    670   ImGui::PushStyleColor(ImGuiCol_Button, UIPrimaryDarkColor);
    671   ImGui::PushStyleColor(ImGuiCol_ButtonActive, UIPrimaryColor);
    672   ImGui::PushStyleColor(ImGuiCol_ButtonHovered, UIPrimaryLightColor);
    673   ImGui::PushStyleColor(ImGuiCol_Border, UIPrimaryLightColor);
    674 }
    675 
    676 void ImGuiFullscreen::PopPrimaryColor()
    677 {
    678   ImGui::PopStyleColor(5);
    679 }
    680 
    681 bool ImGuiFullscreen::BeginFullscreenColumns(const char* title, float pos_y, bool expand_to_screen_width, bool footer)
    682 {
    683   ImGui::SetNextWindowPos(ImVec2(expand_to_screen_width ? 0.0f : g_layout_padding_left, pos_y));
    684   ImGui::SetNextWindowSize(
    685     ImVec2(expand_to_screen_width ? ImGui::GetIO().DisplaySize.x : LayoutScale(LAYOUT_SCREEN_WIDTH),
    686            ImGui::GetIO().DisplaySize.y - pos_y - (footer ? LayoutScale(LAYOUT_FOOTER_HEIGHT) : 0.0f)));
    687 
    688   ImGui::PushStyleVar(ImGuiStyleVar_WindowPadding, ImVec2(0.0f, 0.0f));
    689   ImGui::PushStyleVar(ImGuiStyleVar_WindowBorderSize, 0.0f);
    690   ImGui::PushStyleVar(ImGuiStyleVar_WindowRounding, 0.0f);
    691 
    692   bool clipped;
    693   if (title)
    694   {
    695     ImGui::PushFont(g_large_font);
    696     clipped = ImGui::Begin(title, nullptr, ImGuiWindowFlags_NoCollapse | ImGuiWindowFlags_NoResize);
    697     ImGui::PopFont();
    698   }
    699   else
    700   {
    701     clipped = ImGui::Begin("fullscreen_ui_columns_parent", nullptr,
    702                            ImGuiWindowFlags_NoTitleBar | ImGuiWindowFlags_NoCollapse | ImGuiWindowFlags_NoResize);
    703   }
    704 
    705   return clipped;
    706 }
    707 
    708 void ImGuiFullscreen::EndFullscreenColumns()
    709 {
    710   ImGui::End();
    711   ImGui::PopStyleVar(3);
    712 }
    713 
    714 bool ImGuiFullscreen::BeginFullscreenColumnWindow(float start, float end, const char* name, const ImVec4& background)
    715 {
    716   start = LayoutScale(start);
    717   end = LayoutScale(end);
    718 
    719   if (start < 0.0f)
    720     start = ImGui::GetIO().DisplaySize.x + start;
    721   if (end <= 0.0f)
    722     end = ImGui::GetIO().DisplaySize.x + end;
    723 
    724   const ImVec2 pos(start, 0.0f);
    725   const ImVec2 size(end - start, ImGui::GetCurrentWindow()->Size.y);
    726 
    727   ImGui::PushStyleColor(ImGuiCol_ChildBg, background);
    728 
    729   ImGui::SetCursorPos(pos);
    730 
    731   return ImGui::BeginChild(name, size, false, ImGuiWindowFlags_NavFlattened);
    732 }
    733 
    734 void ImGuiFullscreen::EndFullscreenColumnWindow()
    735 {
    736   ImGui::EndChild();
    737   ImGui::PopStyleColor();
    738 }
    739 
    740 bool ImGuiFullscreen::BeginFullscreenWindow(float left, float top, float width, float height, const char* name,
    741                                             const ImVec4& background /* = HEX_TO_IMVEC4(0x212121, 0xFF) */,
    742                                             float rounding /*= 0.0f*/, const ImVec2& padding /*= 0.0f*/,
    743                                             ImGuiWindowFlags flags /*= 0*/)
    744 {
    745   if (left < 0.0f)
    746     left = (LAYOUT_SCREEN_WIDTH - width) * -left;
    747   if (top < 0.0f)
    748     top = (LAYOUT_SCREEN_HEIGHT - height) * -top;
    749 
    750   const ImVec2 pos(ImVec2(LayoutScale(left) + g_layout_padding_left, LayoutScale(top) + g_layout_padding_top));
    751   const ImVec2 size(LayoutScale(ImVec2(width, height)));
    752   return BeginFullscreenWindow(pos, size, name, background, rounding, padding, flags);
    753 }
    754 
    755 bool ImGuiFullscreen::BeginFullscreenWindow(const ImVec2& position, const ImVec2& size, const char* name,
    756                                             const ImVec4& background /* = HEX_TO_IMVEC4(0x212121, 0xFF) */,
    757                                             float rounding /*= 0.0f*/, const ImVec2& padding /*= 0.0f*/,
    758                                             ImGuiWindowFlags flags /*= 0*/)
    759 {
    760   ImGui::SetNextWindowPos(position);
    761   ImGui::SetNextWindowSize(size);
    762 
    763   ImGui::PushStyleColor(ImGuiCol_WindowBg, background);
    764   ImGui::PushStyleVar(ImGuiStyleVar_WindowPadding, LayoutScale(padding));
    765   ImGui::PushStyleVar(ImGuiStyleVar_WindowBorderSize, 0.0f);
    766   ImGui::PushStyleVar(ImGuiStyleVar_WindowRounding, LayoutScale(rounding));
    767 
    768   return ImGui::Begin(name, nullptr,
    769                       ImGuiWindowFlags_NoTitleBar | ImGuiWindowFlags_NoCollapse | ImGuiWindowFlags_NoResize |
    770                         ImGuiWindowFlags_NoBringToFrontOnFocus | flags);
    771 }
    772 
    773 void ImGuiFullscreen::EndFullscreenWindow()
    774 {
    775   ImGui::End();
    776   ImGui::PopStyleVar(3);
    777   ImGui::PopStyleColor();
    778 }
    779 
    780 bool ImGuiFullscreen::IsGamepadInputSource()
    781 {
    782   return (ImGui::GetCurrentContext()->NavInputSource == ImGuiInputSource_Gamepad);
    783 }
    784 
    785 void ImGuiFullscreen::CreateFooterTextString(SmallStringBase& dest,
    786                                              std::span<const std::pair<const char*, std::string_view>> items)
    787 {
    788   dest.clear();
    789   for (const auto& [icon, text] : items)
    790   {
    791     if (!dest.empty())
    792       dest.append("    ");
    793 
    794     dest.append(icon);
    795     dest.append(' ');
    796     dest.append(text);
    797   }
    798 }
    799 
    800 void ImGuiFullscreen::SetFullscreenFooterText(std::string_view text)
    801 {
    802   s_fullscreen_footer_text.assign(text);
    803 }
    804 
    805 void ImGuiFullscreen::SetFullscreenFooterText(std::span<const std::pair<const char*, std::string_view>> items)
    806 {
    807   CreateFooterTextString(s_fullscreen_footer_text, items);
    808 }
    809 
    810 void ImGuiFullscreen::DrawFullscreenFooter()
    811 {
    812   const ImGuiIO& io = ImGui::GetIO();
    813   if (s_fullscreen_footer_text.empty())
    814   {
    815     s_last_fullscreen_footer_text.clear();
    816     return;
    817   }
    818 
    819   const float padding = LayoutScale(LAYOUT_FOOTER_PADDING);
    820   const float height = LayoutScale(LAYOUT_FOOTER_HEIGHT);
    821 
    822   ImDrawList* dl = ImGui::GetForegroundDrawList();
    823   dl->AddRectFilled(ImVec2(0.0f, io.DisplaySize.y - height), io.DisplaySize, ImGui::GetColorU32(UIPrimaryColor), 0.0f);
    824 
    825   ImFont* const font = g_medium_font;
    826   const float max_width = io.DisplaySize.x - padding * 2.0f;
    827 
    828   float prev_opacity = 0.0f;
    829   if (!s_last_fullscreen_footer_text.empty() && s_fullscreen_footer_text != s_last_fullscreen_footer_text)
    830   {
    831     if (s_fullscreen_text_change_time == 0.0f)
    832       s_fullscreen_text_change_time = 0.15f;
    833     else
    834       s_fullscreen_text_change_time = std::max(s_fullscreen_text_change_time - io.DeltaTime, 0.0f);
    835 
    836     if (s_fullscreen_text_change_time == 0.0f)
    837       s_last_fullscreen_footer_text = s_fullscreen_footer_text;
    838 
    839     prev_opacity = s_fullscreen_text_change_time * (1.0f / 0.15f);
    840     if (prev_opacity > 0.0f)
    841     {
    842       const ImVec2 text_size =
    843         font->CalcTextSizeA(font->FontSize, max_width, 0.0f, s_last_fullscreen_footer_text.c_str(),
    844                             s_last_fullscreen_footer_text.end_ptr());
    845       dl->AddText(
    846         font, font->FontSize,
    847         ImVec2(io.DisplaySize.x - padding * 2.0f - text_size.x, io.DisplaySize.y - font->FontSize - padding),
    848         ImGui::GetColorU32(ImVec4(UIPrimaryTextColor.x, UIPrimaryTextColor.y, UIPrimaryTextColor.z, prev_opacity)),
    849         s_last_fullscreen_footer_text.c_str(), s_last_fullscreen_footer_text.end_ptr());
    850     }
    851   }
    852   else if (s_last_fullscreen_footer_text.empty())
    853   {
    854     s_last_fullscreen_footer_text = s_fullscreen_footer_text;
    855   }
    856 
    857   if (prev_opacity < 1.0f)
    858   {
    859     const ImVec2 text_size = font->CalcTextSizeA(font->FontSize, max_width, 0.0f, s_fullscreen_footer_text.c_str(),
    860                                                  s_fullscreen_footer_text.end_ptr());
    861     dl->AddText(
    862       font, font->FontSize,
    863       ImVec2(io.DisplaySize.x - padding * 2.0f - text_size.x, io.DisplaySize.y - font->FontSize - padding),
    864       ImGui::GetColorU32(ImVec4(UIPrimaryTextColor.x, UIPrimaryTextColor.y, UIPrimaryTextColor.z, 1.0f - prev_opacity)),
    865       s_fullscreen_footer_text.c_str(), s_fullscreen_footer_text.end_ptr());
    866   }
    867 }
    868 
    869 void ImGuiFullscreen::PrerenderMenuButtonBorder()
    870 {
    871   if (!s_had_hovered_menu_item)
    872     return;
    873 
    874   // updating might finish the animation
    875   const ImVec2& min = s_menu_button_frame_min_animated.UpdateAndGetValue();
    876   const ImVec2& max = s_menu_button_frame_max_animated.UpdateAndGetValue();
    877   const ImU32 col = ImGui::GetColorU32(ImGuiCol_ButtonHovered);
    878 
    879   const float t = static_cast<float>(std::min(std::abs(std::sin(ImGui::GetTime() * 0.75) * 1.1), 1.0));
    880   ImGui::PushStyleColor(ImGuiCol_Border, ImGui::GetColorU32(ImGuiCol_Border, t));
    881 
    882   ImGui::RenderFrame(min, max, col, true, 0.0f);
    883 
    884   ImGui::PopStyleColor();
    885 
    886   s_rendered_menu_item_border = true;
    887 }
    888 
    889 void ImGuiFullscreen::BeginMenuButtons(u32 num_items, float y_align, float x_padding, float y_padding,
    890                                        float item_height)
    891 {
    892   s_menu_button_index = 0;
    893 
    894   ImGui::PushStyleVar(ImGuiStyleVar_FramePadding, LayoutScale(x_padding, y_padding));
    895   ImGui::PushStyleVar(ImGuiStyleVar_FrameRounding, 0.0f);
    896   ImGui::PushStyleVar(ImGuiStyleVar_FrameBorderSize, LayoutScale(1.0f));
    897   ImGui::PushStyleVar(ImGuiStyleVar_ItemSpacing, ImVec2(0.0f, 0.0f));
    898 
    899   if (y_align != 0.0f)
    900   {
    901     const float real_item_height = LayoutScale(item_height) + (LayoutScale(y_padding) * 2.0f);
    902     const float total_size = (static_cast<float>(num_items) * real_item_height) + (LayoutScale(y_padding) * 2.0f);
    903     const float window_height = ImGui::GetWindowHeight();
    904     if (window_height > total_size)
    905       ImGui::SetCursorPosY((window_height - total_size) * y_align);
    906   }
    907 
    908   PrerenderMenuButtonBorder();
    909 }
    910 
    911 void ImGuiFullscreen::EndMenuButtons()
    912 {
    913   ImGui::PopStyleVar(4);
    914 }
    915 
    916 void ImGuiFullscreen::DrawWindowTitle(const char* title)
    917 {
    918   ImGuiWindow* window = ImGui::GetCurrentWindow();
    919   const ImVec2 pos(window->DC.CursorPos + LayoutScale(LAYOUT_MENU_BUTTON_X_PADDING, LAYOUT_MENU_BUTTON_Y_PADDING));
    920   const ImVec2 size(window->WorkRect.GetWidth() - (LayoutScale(LAYOUT_MENU_BUTTON_X_PADDING) * 2.0f),
    921                     g_large_font->FontSize + LayoutScale(LAYOUT_MENU_BUTTON_Y_PADDING) * 2.0f);
    922   const ImRect rect(pos, pos + size);
    923 
    924   ImGui::ItemSize(size);
    925   if (!ImGui::ItemAdd(rect, window->GetID("window_title")))
    926     return;
    927 
    928   ImGui::PushFont(g_large_font);
    929   ImGui::RenderTextClipped(rect.Min, rect.Max, title, nullptr, nullptr, ImVec2(0.0f, 0.0f), &rect);
    930   ImGui::PopFont();
    931 
    932   const ImVec2 line_start(pos.x, pos.y + g_large_font->FontSize + LayoutScale(LAYOUT_MENU_BUTTON_Y_PADDING));
    933   const ImVec2 line_end(pos.x + size.x, line_start.y);
    934   const float line_thickness = LayoutScale(1.0f);
    935   ImDrawList* dl = ImGui::GetWindowDrawList();
    936   dl->AddLine(line_start, line_end, IM_COL32(255, 255, 255, 255), line_thickness);
    937 }
    938 
    939 void ImGuiFullscreen::GetMenuButtonFrameBounds(float height, ImVec2* pos, ImVec2* size)
    940 {
    941   ImGuiWindow* window = ImGui::GetCurrentWindow();
    942   *pos = window->DC.CursorPos;
    943   *size = ImVec2(window->WorkRect.GetWidth(), LayoutScale(height) + ImGui::GetStyle().FramePadding.y * 2.0f);
    944 }
    945 
    946 bool ImGuiFullscreen::MenuButtonFrame(const char* str_id, bool enabled, float height, bool* visible, bool* hovered,
    947                                       ImRect* bb, ImGuiButtonFlags flags, float hover_alpha)
    948 {
    949   ImGuiWindow* window = ImGui::GetCurrentWindow();
    950   if (window->SkipItems)
    951   {
    952     *visible = false;
    953     *hovered = false;
    954     return false;
    955   }
    956 
    957   ImVec2 pos, size;
    958   GetMenuButtonFrameBounds(height, &pos, &size);
    959   *bb = ImRect(pos, pos + size);
    960 
    961   const ImGuiID id = window->GetID(str_id);
    962   ImGui::ItemSize(size);
    963   if (enabled)
    964   {
    965     if (!ImGui::ItemAdd(*bb, id))
    966     {
    967       *visible = false;
    968       *hovered = false;
    969       return false;
    970     }
    971   }
    972   else
    973   {
    974     if (ImGui::IsClippedEx(*bb, id))
    975     {
    976       *visible = false;
    977       *hovered = false;
    978       return false;
    979     }
    980   }
    981 
    982   *visible = true;
    983 
    984   bool held;
    985   bool pressed;
    986   if (enabled)
    987   {
    988     pressed = ImGui::ButtonBehavior(*bb, id, hovered, &held, flags);
    989     if (*hovered)
    990     {
    991       const ImU32 col = ImGui::GetColorU32(held ? ImGuiCol_ButtonActive : ImGuiCol_ButtonHovered, hover_alpha);
    992 
    993       const float t = static_cast<float>(std::min(std::abs(std::sin(ImGui::GetTime() * 0.75) * 1.1), 1.0));
    994       ImGui::PushStyleColor(ImGuiCol_Border, ImGui::GetColorU32(ImGuiCol_Border, t));
    995 
    996       DrawMenuButtonFrame(bb->Min, bb->Max, col, true, 0.0f);
    997 
    998       ImGui::PopStyleColor();
    999     }
   1000   }
   1001   else
   1002   {
   1003     pressed = false;
   1004     held = false;
   1005   }
   1006 
   1007   const ImGuiStyle& style = ImGui::GetStyle();
   1008   bb->Min += style.FramePadding;
   1009   bb->Max -= style.FramePadding;
   1010 
   1011   return pressed;
   1012 }
   1013 
   1014 void ImGuiFullscreen::DrawMenuButtonFrame(const ImVec2& p_min, const ImVec2& p_max, ImU32 fill_col,
   1015                                           bool border /* = true */, float rounding /* = 0.0f */)
   1016 {
   1017   ImVec2 frame_min = p_min;
   1018   ImVec2 frame_max = p_max;
   1019 
   1020   const ImGuiIO& io = ImGui::GetIO();
   1021   if (io.NavVisible)
   1022   {
   1023     if (!s_had_hovered_menu_item || io.MouseDelta.x != 0.0f || io.MouseDelta.y != 0.0f)
   1024     {
   1025       s_menu_button_frame_min_animated.Reset(frame_min);
   1026       s_menu_button_frame_max_animated.Reset(frame_max);
   1027       s_has_hovered_menu_item = true;
   1028     }
   1029     else
   1030     {
   1031       if (frame_min.x != s_menu_button_frame_min_animated.GetEndValue().x ||
   1032           frame_min.y != s_menu_button_frame_min_animated.GetEndValue().y)
   1033       {
   1034         s_menu_button_frame_min_animated.Start(s_menu_button_frame_min_animated.GetCurrentValue(), frame_min,
   1035                                                MENU_BACKGROUND_ANIMATION_TIME);
   1036       }
   1037       if (frame_max.x != s_menu_button_frame_max_animated.GetEndValue().x ||
   1038           frame_max.y != s_menu_button_frame_max_animated.GetEndValue().y)
   1039       {
   1040         s_menu_button_frame_max_animated.Start(s_menu_button_frame_max_animated.GetCurrentValue(), frame_max,
   1041                                                MENU_BACKGROUND_ANIMATION_TIME);
   1042       }
   1043       frame_min = s_menu_button_frame_min_animated.UpdateAndGetValue();
   1044       frame_max = s_menu_button_frame_max_animated.UpdateAndGetValue();
   1045       s_has_hovered_menu_item = true;
   1046     }
   1047   }
   1048 
   1049   if (!s_rendered_menu_item_border)
   1050   {
   1051     s_rendered_menu_item_border = true;
   1052     ImGui::RenderFrame(frame_min, frame_max, fill_col, border, rounding);
   1053   }
   1054 }
   1055 
   1056 bool ImGuiFullscreen::MenuButtonFrame(const char* str_id, bool enabled, float height, bool* visible, bool* hovered,
   1057                                       ImVec2* min, ImVec2* max, ImGuiButtonFlags flags /*= 0*/,
   1058                                       float hover_alpha /*= 0*/)
   1059 {
   1060   ImRect bb;
   1061   const bool result = MenuButtonFrame(str_id, enabled, height, visible, hovered, &bb, flags, hover_alpha);
   1062   *min = bb.Min;
   1063   *max = bb.Max;
   1064   return result;
   1065 }
   1066 
   1067 void ImGuiFullscreen::ResetMenuButtonFrame()
   1068 {
   1069   s_had_hovered_menu_item = false;
   1070   s_has_hovered_menu_item = false;
   1071 }
   1072 
   1073 void ImGuiFullscreen::MenuHeading(const char* title, bool draw_line /*= true*/)
   1074 {
   1075   const float line_thickness = draw_line ? LayoutScale(1.0f) : 0.0f;
   1076   const float line_padding = draw_line ? LayoutScale(5.0f) : 0.0f;
   1077 
   1078   bool visible, hovered;
   1079   ImRect bb;
   1080   MenuButtonFrame(title, false, LAYOUT_MENU_BUTTON_HEIGHT_NO_SUMMARY, &visible, &hovered, &bb);
   1081   if (!visible)
   1082     return;
   1083 
   1084   ImGui::PushStyleColor(ImGuiCol_Text, ImGui::GetColorU32(ImGuiCol_TextDisabled));
   1085   ImGui::PushFont(g_large_font);
   1086   ImGui::RenderTextClipped(bb.Min, bb.Max, title, nullptr, nullptr, ImVec2(0.0f, 0.0f), &bb);
   1087   ImGui::PopFont();
   1088   ImGui::PopStyleColor();
   1089 
   1090   if (draw_line)
   1091   {
   1092     const ImVec2 line_start(bb.Min.x, bb.Min.y + g_large_font->FontSize + line_padding);
   1093     const ImVec2 line_end(bb.Max.x, line_start.y);
   1094     ImGui::GetWindowDrawList()->AddLine(line_start, line_end, ImGui::GetColorU32(ImGuiCol_TextDisabled),
   1095                                         line_thickness);
   1096   }
   1097 }
   1098 
   1099 bool ImGuiFullscreen::MenuHeadingButton(const char* title, const char* value /*= nullptr*/, bool enabled /*= true*/,
   1100                                         bool draw_line /*= true*/)
   1101 {
   1102   const float line_thickness = draw_line ? LayoutScale(1.0f) : 0.0f;
   1103   const float line_padding = draw_line ? LayoutScale(5.0f) : 0.0f;
   1104 
   1105   ImRect bb;
   1106   bool visible, hovered;
   1107   bool pressed = MenuButtonFrame(title, enabled, LAYOUT_MENU_BUTTON_HEIGHT_NO_SUMMARY, &visible, &hovered, &bb);
   1108   if (!visible)
   1109     return false;
   1110 
   1111   if (!enabled)
   1112     ImGui::PushStyleColor(ImGuiCol_Text, ImGui::GetColorU32(ImGuiCol_TextDisabled));
   1113   ImGui::PushFont(g_large_font);
   1114   ImGui::RenderTextClipped(bb.Min, bb.Max, title, nullptr, nullptr, ImVec2(0.0f, 0.0f), &bb);
   1115 
   1116   if (value)
   1117   {
   1118     const ImVec2 value_size(
   1119       g_large_font->CalcTextSizeA(g_large_font->FontSize, std::numeric_limits<float>::max(), 0.0f, value));
   1120     const ImRect value_bb(ImVec2(bb.Max.x - value_size.x, bb.Min.y), ImVec2(bb.Max.x, bb.Max.y));
   1121     ImGui::RenderTextClipped(value_bb.Min, value_bb.Max, value, nullptr, nullptr, ImVec2(0.0f, 0.0f), &value_bb);
   1122   }
   1123 
   1124   ImGui::PopFont();
   1125   if (!enabled)
   1126     ImGui::PopStyleColor();
   1127 
   1128   if (draw_line)
   1129   {
   1130     const ImVec2 line_start(bb.Min.x, bb.Min.y + g_large_font->FontSize + line_padding);
   1131     const ImVec2 line_end(bb.Max.x, line_start.y);
   1132     ImGui::GetWindowDrawList()->AddLine(line_start, line_end, ImGui::GetColorU32(ImGuiCol_TextDisabled),
   1133                                         line_thickness);
   1134   }
   1135 
   1136   return pressed;
   1137 }
   1138 
   1139 bool ImGuiFullscreen::ActiveButton(const char* title, bool is_active, bool enabled, float height, ImFont* font)
   1140 {
   1141   return ActiveButtonWithRightText(title, nullptr, is_active, enabled, height, font);
   1142 }
   1143 
   1144 bool ImGuiFullscreen::DefaultActiveButton(const char* title, bool is_active, bool enabled /* = true */,
   1145                                           float height /* = LAYOUT_MENU_BUTTON_HEIGHT_NO_SUMMARY */,
   1146                                           ImFont* font /* = g_large_font */)
   1147 {
   1148   const bool result = ActiveButtonWithRightText(title, nullptr, is_active, enabled, height, font);
   1149   ImGui::SetItemDefaultFocus();
   1150   return result;
   1151 }
   1152 
   1153 bool ImGuiFullscreen::ActiveButtonWithRightText(const char* title, const char* right_title, bool is_active,
   1154                                                 bool enabled, float height, ImFont* font)
   1155 {
   1156   if (is_active)
   1157   {
   1158     // don't draw over a prerendered border
   1159     const float border_size = ImGui::GetStyle().FrameBorderSize;
   1160     const ImVec2 border_size_v = ImVec2(border_size, border_size);
   1161     ImVec2 pos, size;
   1162     GetMenuButtonFrameBounds(height, &pos, &size);
   1163     ImGui::RenderFrame(pos + border_size_v, pos + size - border_size_v, ImGui::GetColorU32(UIPrimaryColor), false);
   1164   }
   1165 
   1166   ImRect bb;
   1167   bool visible, hovered;
   1168   bool pressed = MenuButtonFrame(title, enabled, height, &visible, &hovered, &bb);
   1169   if (!visible)
   1170     return false;
   1171 
   1172   const ImRect title_bb(bb.GetTL(), bb.GetBR());
   1173 
   1174   if (!enabled)
   1175     ImGui::PushStyleColor(ImGuiCol_Text, ImGui::GetColorU32(ImGuiCol_TextDisabled));
   1176 
   1177   ImGui::PushFont(font);
   1178   ImGui::RenderTextClipped(title_bb.Min, title_bb.Max, title, nullptr, nullptr, ImVec2(0.0f, 0.0f), &title_bb);
   1179 
   1180   if (right_title && *right_title)
   1181   {
   1182     const ImVec2 right_text_size = font->CalcTextSizeA(font->FontSize, title_bb.GetWidth(), 0.0f, right_title);
   1183     const ImVec2 right_text_start = ImVec2(title_bb.Max.x - right_text_size.x, title_bb.Min.y);
   1184     ImGui::RenderTextClipped(right_text_start, title_bb.Max, right_title, nullptr, &right_text_size, ImVec2(0.0f, 0.0f),
   1185                              &title_bb);
   1186   }
   1187 
   1188   ImGui::PopFont();
   1189 
   1190   if (!enabled)
   1191     ImGui::PopStyleColor();
   1192 
   1193   s_menu_button_index++;
   1194   return pressed;
   1195 }
   1196 
   1197 bool ImGuiFullscreen::MenuButton(const char* title, const char* summary, bool enabled, float height, ImFont* font,
   1198                                  ImFont* summary_font)
   1199 {
   1200   ImRect bb;
   1201   bool visible, hovered;
   1202   bool pressed = MenuButtonFrame(title, enabled, height, &visible, &hovered, &bb);
   1203   if (!visible)
   1204     return false;
   1205 
   1206   const float midpoint = bb.Min.y + font->FontSize + LayoutScale(4.0f);
   1207   const ImRect title_bb(bb.Min, ImVec2(bb.Max.x, midpoint));
   1208   const ImRect summary_bb(ImVec2(bb.Min.x, midpoint), bb.Max);
   1209 
   1210   if (!enabled)
   1211     ImGui::PushStyleColor(ImGuiCol_Text, ImGui::GetColorU32(ImGuiCol_TextDisabled));
   1212 
   1213   ImGui::PushFont(font);
   1214   ImGui::RenderTextClipped(title_bb.Min, title_bb.Max, title, nullptr, nullptr, ImVec2(0.0f, 0.0f), &title_bb);
   1215   ImGui::PopFont();
   1216 
   1217   if (summary)
   1218   {
   1219     ImGui::PushFont(summary_font);
   1220     ImGui::RenderTextClipped(summary_bb.Min, summary_bb.Max, summary, nullptr, nullptr, ImVec2(0.0f, 0.0f),
   1221                              &summary_bb);
   1222     ImGui::PopFont();
   1223   }
   1224 
   1225   if (!enabled)
   1226     ImGui::PopStyleColor();
   1227 
   1228   s_menu_button_index++;
   1229   return pressed;
   1230 }
   1231 
   1232 bool ImGuiFullscreen::MenuButtonWithoutSummary(const char* title, bool enabled, float height, ImFont* font,
   1233                                                const ImVec2& text_align)
   1234 {
   1235   ImRect bb;
   1236   bool visible, hovered;
   1237   bool pressed = MenuButtonFrame(title, enabled, height, &visible, &hovered, &bb);
   1238   if (!visible)
   1239     return false;
   1240 
   1241   const float midpoint = bb.Min.y + font->FontSize + LayoutScale(4.0f);
   1242   const ImRect title_bb(bb.Min, ImVec2(bb.Max.x, midpoint));
   1243   const ImRect summary_bb(ImVec2(bb.Min.x, midpoint), bb.Max);
   1244 
   1245   if (!enabled)
   1246     ImGui::PushStyleColor(ImGuiCol_Text, ImGui::GetColorU32(ImGuiCol_TextDisabled));
   1247 
   1248   ImGui::PushFont(font);
   1249   ImGui::RenderTextClipped(title_bb.Min, title_bb.Max, title, nullptr, nullptr, text_align, &title_bb);
   1250   ImGui::PopFont();
   1251 
   1252   if (!enabled)
   1253     ImGui::PopStyleColor();
   1254 
   1255   s_menu_button_index++;
   1256   return pressed;
   1257 }
   1258 
   1259 bool ImGuiFullscreen::MenuImageButton(const char* title, const char* summary, ImTextureID user_texture_id,
   1260                                       const ImVec2& image_size, bool enabled, float height, const ImVec2& uv0,
   1261                                       const ImVec2& uv1, ImFont* title_font, ImFont* summary_font)
   1262 {
   1263   ImRect bb;
   1264   bool visible, hovered;
   1265   bool pressed = MenuButtonFrame(title, enabled, height, &visible, &hovered, &bb);
   1266   if (!visible)
   1267     return false;
   1268 
   1269   ImGui::GetWindowDrawList()->AddImage(user_texture_id, bb.Min, bb.Min + image_size, uv0, uv1,
   1270                                        enabled ? IM_COL32(255, 255, 255, 255) :
   1271                                                  ImGui::GetColorU32(ImGuiCol_TextDisabled));
   1272 
   1273   const float midpoint = bb.Min.y + title_font->FontSize + LayoutScale(4.0f);
   1274   const float text_start_x = bb.Min.x + image_size.x + LayoutScale(15.0f);
   1275   const ImRect title_bb(ImVec2(text_start_x, bb.Min.y), ImVec2(bb.Max.x, midpoint));
   1276   const ImRect summary_bb(ImVec2(text_start_x, midpoint), bb.Max);
   1277 
   1278   if (!enabled)
   1279     ImGui::PushStyleColor(ImGuiCol_Text, ImGui::GetColorU32(ImGuiCol_TextDisabled));
   1280 
   1281   ImGui::PushFont(title_font);
   1282   ImGui::RenderTextClipped(title_bb.Min, title_bb.Max, title, nullptr, nullptr, ImVec2(0.0f, 0.0f), &title_bb);
   1283   ImGui::PopFont();
   1284 
   1285   if (summary)
   1286   {
   1287     ImGui::PushFont(summary_font);
   1288     ImGui::RenderTextClipped(summary_bb.Min, summary_bb.Max, summary, nullptr, nullptr, ImVec2(0.0f, 0.0f),
   1289                              &summary_bb);
   1290     ImGui::PopFont();
   1291   }
   1292 
   1293   if (!enabled)
   1294     ImGui::PopStyleColor();
   1295 
   1296   s_menu_button_index++;
   1297   return pressed;
   1298 }
   1299 
   1300 bool ImGuiFullscreen::FloatingButton(const char* text, float x, float y, float width, float height, float anchor_x,
   1301                                      float anchor_y, bool enabled, ImFont* font, ImVec2* out_position,
   1302                                      bool repeat_button)
   1303 {
   1304   const ImVec2 text_size(font->CalcTextSizeA(font->FontSize, std::numeric_limits<float>::max(), 0.0f, text));
   1305   const ImVec2& padding(ImGui::GetStyle().FramePadding);
   1306   if (width < 0.0f)
   1307     width = (padding.x * 2.0f) + text_size.x;
   1308   if (height < 0.0f)
   1309     height = (padding.y * 2.0f) + text_size.y;
   1310 
   1311   const ImVec2 window_size(ImGui::GetWindowSize());
   1312   if (anchor_x == -1.0f)
   1313     x -= width;
   1314   else if (anchor_x == -0.5f)
   1315     x -= (width * 0.5f);
   1316   else if (anchor_x == 0.5f)
   1317     x = (window_size.x * 0.5f) - (width * 0.5f) - x;
   1318   else if (anchor_x == 1.0f)
   1319     x = window_size.x - width - x;
   1320   if (anchor_y == -1.0f)
   1321     y -= height;
   1322   else if (anchor_y == -0.5f)
   1323     y -= (height * 0.5f);
   1324   else if (anchor_y == 0.5f)
   1325     y = (window_size.y * 0.5f) - (height * 0.5f) - y;
   1326   else if (anchor_y == 1.0f)
   1327     y = window_size.y - height - y;
   1328 
   1329   if (out_position)
   1330     *out_position = ImVec2(x, y);
   1331 
   1332   ImGuiWindow* window = ImGui::GetCurrentWindow();
   1333   if (window->SkipItems)
   1334     return false;
   1335 
   1336   const ImVec2 base(ImGui::GetWindowPos() + ImVec2(x, y));
   1337   ImRect bb(base, base + ImVec2(width, height));
   1338 
   1339   const ImGuiID id = window->GetID(text);
   1340   if (enabled)
   1341   {
   1342     if (!ImGui::ItemAdd(bb, id))
   1343       return false;
   1344   }
   1345   else
   1346   {
   1347     if (ImGui::IsClippedEx(bb, id))
   1348       return false;
   1349   }
   1350 
   1351   bool hovered;
   1352   bool held;
   1353   bool pressed;
   1354   if (enabled)
   1355   {
   1356     pressed = ImGui::ButtonBehavior(bb, id, &hovered, &held, repeat_button ? ImGuiButtonFlags_Repeat : 0);
   1357     if (hovered)
   1358     {
   1359       const float t = std::min(static_cast<float>(std::abs(std::sin(ImGui::GetTime() * 0.75) * 1.1)), 1.0f);
   1360       const ImU32 col = ImGui::GetColorU32(held ? ImGuiCol_ButtonActive : ImGuiCol_ButtonHovered, 1.0f);
   1361       ImGui::PushStyleColor(ImGuiCol_Border, ImGui::GetColorU32(ImGuiCol_Border, t));
   1362       DrawMenuButtonFrame(bb.Min, bb.Max, col, true, 0.0f);
   1363       ImGui::PopStyleColor();
   1364     }
   1365   }
   1366   else
   1367   {
   1368     hovered = false;
   1369     pressed = false;
   1370     held = false;
   1371   }
   1372 
   1373   bb.Min += padding;
   1374   bb.Max -= padding;
   1375 
   1376   if (!enabled)
   1377     ImGui::PushStyleColor(ImGuiCol_Text, ImGui::GetColorU32(ImGuiCol_TextDisabled));
   1378 
   1379   ImGui::PushFont(font);
   1380   ImGui::RenderTextClipped(bb.Min, bb.Max, text, nullptr, nullptr, ImVec2(0.0f, 0.0f), &bb);
   1381   ImGui::PopFont();
   1382 
   1383   if (!enabled)
   1384     ImGui::PopStyleColor();
   1385 
   1386   return pressed;
   1387 }
   1388 
   1389 bool ImGuiFullscreen::ToggleButton(const char* title, const char* summary, bool* v, bool enabled, float height,
   1390                                    ImFont* font, ImFont* summary_font)
   1391 {
   1392   ImRect bb;
   1393   bool visible, hovered;
   1394   bool pressed = MenuButtonFrame(title, enabled, height, &visible, &hovered, &bb, ImGuiButtonFlags_PressedOnClick);
   1395   if (!visible)
   1396     return false;
   1397 
   1398   const float midpoint = bb.Min.y + font->FontSize + LayoutScale(4.0f);
   1399   const ImRect title_bb(bb.Min, ImVec2(bb.Max.x, midpoint));
   1400   const ImRect summary_bb(ImVec2(bb.Min.x, midpoint), bb.Max);
   1401 
   1402   if (!enabled)
   1403     ImGui::PushStyleColor(ImGuiCol_Text, ImGui::GetColorU32(ImGuiCol_TextDisabled));
   1404 
   1405   ImGui::PushFont(font);
   1406   ImGui::RenderTextClipped(title_bb.Min, title_bb.Max, title, nullptr, nullptr, ImVec2(0.0f, 0.0f), &title_bb);
   1407   ImGui::PopFont();
   1408 
   1409   if (summary)
   1410   {
   1411     ImGui::PushFont(summary_font);
   1412     ImGui::RenderTextClipped(summary_bb.Min, summary_bb.Max, summary, nullptr, nullptr, ImVec2(0.0f, 0.0f),
   1413                              &summary_bb);
   1414     ImGui::PopFont();
   1415   }
   1416 
   1417   if (!enabled)
   1418     ImGui::PopStyleColor();
   1419 
   1420   const float toggle_width = LayoutScale(50.0f);
   1421   const float toggle_height = LayoutScale(25.0f);
   1422   const float toggle_x = LayoutScale(8.0f);
   1423   const float toggle_y = (LayoutScale(height) - toggle_height) * 0.5f;
   1424   const float toggle_radius = toggle_height * 0.5f;
   1425   const ImVec2 toggle_pos(bb.Max.x - toggle_width - toggle_x, bb.Min.y + toggle_y);
   1426 
   1427   if (pressed)
   1428     *v = !*v;
   1429 
   1430   float t = *v ? 1.0f : 0.0f;
   1431   ImDrawList* dl = ImGui::GetWindowDrawList();
   1432   ImGuiContext& g = *GImGui;
   1433   if (g.LastActiveId == g.CurrentWindow->GetID(title)) // && g.LastActiveIdTimer < ANIM_SPEED)
   1434   {
   1435     static constexpr const float ANIM_SPEED = 0.08f;
   1436     float t_anim = ImSaturate(g.LastActiveIdTimer / ANIM_SPEED);
   1437     t = *v ? (t_anim) : (1.0f - t_anim);
   1438   }
   1439 
   1440   ImU32 col_bg;
   1441   ImU32 col_knob;
   1442   if (!enabled)
   1443   {
   1444     col_bg = ImGui::GetColorU32(UIDisabledColor);
   1445     col_knob = IM_COL32(200, 200, 200, 200);
   1446   }
   1447   else
   1448   {
   1449     col_bg = ImGui::GetColorU32(ImLerp(HEX_TO_IMVEC4(0x8C8C8C, 0xff), UISecondaryStrongColor, t));
   1450     col_knob = IM_COL32(255, 255, 255, 255);
   1451   }
   1452 
   1453   dl->AddRectFilled(toggle_pos, ImVec2(toggle_pos.x + toggle_width, toggle_pos.y + toggle_height), col_bg,
   1454                     toggle_height * 0.5f);
   1455   dl->AddCircleFilled(
   1456     ImVec2(toggle_pos.x + toggle_radius + t * (toggle_width - toggle_radius * 2.0f), toggle_pos.y + toggle_radius),
   1457     toggle_radius - 1.5f, col_knob, 32);
   1458 
   1459   s_menu_button_index++;
   1460   return pressed;
   1461 }
   1462 
   1463 bool ImGuiFullscreen::ThreeWayToggleButton(const char* title, const char* summary, std::optional<bool>* v, bool enabled,
   1464                                            float height, ImFont* font, ImFont* summary_font)
   1465 {
   1466   ImRect bb;
   1467   bool visible, hovered;
   1468   bool pressed = MenuButtonFrame(title, enabled, height, &visible, &hovered, &bb, ImGuiButtonFlags_PressedOnClick);
   1469   if (!visible)
   1470     return false;
   1471 
   1472   const float midpoint = bb.Min.y + font->FontSize + LayoutScale(4.0f);
   1473   const ImRect title_bb(bb.Min, ImVec2(bb.Max.x, midpoint));
   1474   const ImRect summary_bb(ImVec2(bb.Min.x, midpoint), bb.Max);
   1475 
   1476   if (!enabled)
   1477     ImGui::PushStyleColor(ImGuiCol_Text, ImGui::GetColorU32(ImGuiCol_TextDisabled));
   1478 
   1479   ImGui::PushFont(font);
   1480   ImGui::RenderTextClipped(title_bb.Min, title_bb.Max, title, nullptr, nullptr, ImVec2(0.0f, 0.0f), &title_bb);
   1481   ImGui::PopFont();
   1482 
   1483   if (summary)
   1484   {
   1485     ImGui::PushFont(summary_font);
   1486     ImGui::RenderTextClipped(summary_bb.Min, summary_bb.Max, summary, nullptr, nullptr, ImVec2(0.0f, 0.0f),
   1487                              &summary_bb);
   1488     ImGui::PopFont();
   1489   }
   1490 
   1491   if (!enabled)
   1492     ImGui::PopStyleColor();
   1493 
   1494   const float toggle_width = LayoutScale(50.0f);
   1495   const float toggle_height = LayoutScale(25.0f);
   1496   const float toggle_x = LayoutScale(8.0f);
   1497   const float toggle_y = (LayoutScale(LAYOUT_MENU_BUTTON_HEIGHT) - toggle_height) * 0.5f;
   1498   const float toggle_radius = toggle_height * 0.5f;
   1499   const ImVec2 toggle_pos(bb.Max.x - toggle_width - toggle_x, bb.Min.y + toggle_y);
   1500 
   1501   if (pressed)
   1502   {
   1503     if (v->has_value() && v->value())
   1504       *v = false;
   1505     else if (v->has_value() && !v->value())
   1506       v->reset();
   1507     else
   1508       *v = true;
   1509   }
   1510 
   1511   float t = v->has_value() ? (v->value() ? 1.0f : 0.0f) : 0.5f;
   1512   ImDrawList* dl = ImGui::GetWindowDrawList();
   1513   ImGuiContext& g = *GImGui;
   1514   float ANIM_SPEED = 0.08f;
   1515   if (g.LastActiveId == g.CurrentWindow->GetID(title)) // && g.LastActiveIdTimer < ANIM_SPEED)
   1516   {
   1517     float t_anim = ImSaturate(g.LastActiveIdTimer / ANIM_SPEED);
   1518     t = (v->has_value() ? (v->value() ? std::min(t_anim + 0.5f, 1.0f) : (1.0f - t_anim)) : (t_anim * 0.5f));
   1519   }
   1520 
   1521   const float color_t = v->has_value() ? t : 0.0f;
   1522 
   1523   ImU32 col_bg;
   1524   if (!enabled)
   1525     col_bg = IM_COL32(0x75, 0x75, 0x75, 0xff);
   1526   else if (hovered)
   1527     col_bg = ImGui::GetColorU32(ImLerp(v->has_value() ? HEX_TO_IMVEC4(0xf05100, 0xff) : HEX_TO_IMVEC4(0x9e9e9e, 0xff),
   1528                                        UISecondaryStrongColor, color_t));
   1529   else
   1530     col_bg = ImGui::GetColorU32(ImLerp(v->has_value() ? HEX_TO_IMVEC4(0xc45100, 0xff) : HEX_TO_IMVEC4(0x757575, 0xff),
   1531                                        UISecondaryStrongColor, color_t));
   1532 
   1533   dl->AddRectFilled(toggle_pos, ImVec2(toggle_pos.x + toggle_width, toggle_pos.y + toggle_height), col_bg,
   1534                     toggle_height * 0.5f);
   1535   dl->AddCircleFilled(
   1536     ImVec2(toggle_pos.x + toggle_radius + t * (toggle_width - toggle_radius * 2.0f), toggle_pos.y + toggle_radius),
   1537     toggle_radius - 1.5f, IM_COL32(255, 255, 255, 255), 32);
   1538 
   1539   s_menu_button_index++;
   1540   return pressed;
   1541 }
   1542 
   1543 bool ImGuiFullscreen::RangeButton(const char* title, const char* summary, s32* value, s32 min, s32 max, s32 increment,
   1544                                   const char* format, bool enabled /*= true*/,
   1545                                   float height /*= LAYOUT_MENU_BUTTON_HEIGHT*/, ImFont* font /*= g_large_font*/,
   1546                                   ImFont* summary_font /*= g_medium_font*/, const char* ok_text /*= "OK"*/)
   1547 {
   1548   ImRect bb;
   1549   bool visible, hovered;
   1550   bool pressed = MenuButtonFrame(title, enabled, height, &visible, &hovered, &bb);
   1551   if (!visible)
   1552     return false;
   1553 
   1554   const SmallString value_text = SmallString::from_sprintf(format, *value);
   1555   const ImVec2 value_size(ImGui::CalcTextSize(value_text.c_str()));
   1556 
   1557   const float midpoint = bb.Min.y + font->FontSize + LayoutScale(4.0f);
   1558   const float text_end = bb.Max.x - value_size.x;
   1559   const ImRect title_bb(bb.Min, ImVec2(text_end, midpoint));
   1560   const ImRect summary_bb(ImVec2(bb.Min.x, midpoint), ImVec2(text_end, bb.Max.y));
   1561 
   1562   if (!enabled)
   1563     ImGui::PushStyleColor(ImGuiCol_Text, ImGui::GetColorU32(ImGuiCol_TextDisabled));
   1564 
   1565   ImGui::PushFont(font);
   1566   ImGui::RenderTextClipped(title_bb.Min, title_bb.Max, title, nullptr, nullptr, ImVec2(0.0f, 0.0f), &title_bb);
   1567   ImGui::RenderTextClipped(bb.Min, bb.Max, value_text.c_str(), nullptr, nullptr, ImVec2(1.0f, 0.5f), &bb);
   1568   ImGui::PopFont();
   1569 
   1570   if (summary)
   1571   {
   1572     ImGui::PushFont(summary_font);
   1573     ImGui::RenderTextClipped(summary_bb.Min, summary_bb.Max, summary, nullptr, nullptr, ImVec2(0.0f, 0.0f),
   1574                              &summary_bb);
   1575     ImGui::PopFont();
   1576   }
   1577 
   1578   if (!enabled)
   1579     ImGui::PopStyleColor();
   1580 
   1581   if (pressed)
   1582     ImGui::OpenPopup(title);
   1583 
   1584   bool changed = false;
   1585 
   1586   ImGui::SetNextWindowSize(LayoutScale(500.0f, 192.0f));
   1587   ImGui::SetNextWindowPos((ImGui::GetIO().DisplaySize - LayoutScale(0.0f, LAYOUT_FOOTER_HEIGHT)) * 0.5f,
   1588                           ImGuiCond_Always, ImVec2(0.5f, 0.5f));
   1589 
   1590   ImGui::PushFont(g_large_font);
   1591   ImGui::PushStyleVar(ImGuiStyleVar_WindowRounding, LayoutScale(10.0f));
   1592   ImGui::PushStyleVar(ImGuiStyleVar_FramePadding, LayoutScale(ImGuiFullscreen::LAYOUT_MENU_BUTTON_X_PADDING,
   1593                                                               ImGuiFullscreen::LAYOUT_MENU_BUTTON_Y_PADDING));
   1594   ImGui::PushStyleVar(ImGuiStyleVar_FrameBorderSize, 0.0f);
   1595   ImGui::PushStyleVar(ImGuiStyleVar_WindowPadding, LayoutScale(20.0f, 20.0f));
   1596 
   1597   if (ImGui::BeginPopupModal(title, nullptr,
   1598                              ImGuiWindowFlags_NoCollapse | ImGuiWindowFlags_NoResize | ImGuiWindowFlags_NoMove))
   1599   {
   1600     BeginMenuButtons();
   1601 
   1602     const float end = ImGui::GetCurrentWindow()->WorkRect.GetWidth();
   1603     ImGui::SetNextItemWidth(end);
   1604 
   1605     changed = ImGui::SliderInt("##value", value, min, max, format, ImGuiSliderFlags_NoInput);
   1606 
   1607     ImGui::SetCursorPosY(ImGui::GetCursorPosY() + LayoutScale(10.0f));
   1608     if (MenuButtonWithoutSummary(ok_text, true, LAYOUT_MENU_BUTTON_HEIGHT_NO_SUMMARY, g_large_font, ImVec2(0.5f, 0.0f)))
   1609       ImGui::CloseCurrentPopup();
   1610     EndMenuButtons();
   1611 
   1612     ImGui::EndPopup();
   1613   }
   1614 
   1615   ImGui::PopStyleVar(4);
   1616   ImGui::PopFont();
   1617 
   1618   return changed;
   1619 }
   1620 
   1621 bool ImGuiFullscreen::RangeButton(const char* title, const char* summary, float* value, float min, float max,
   1622                                   float increment, const char* format, bool enabled /*= true*/,
   1623                                   float height /*= LAYOUT_MENU_BUTTON_HEIGHT*/, ImFont* font /*= g_large_font*/,
   1624                                   ImFont* summary_font /*= g_medium_font*/, const char* ok_text /*= "OK"*/)
   1625 {
   1626   ImRect bb;
   1627   bool visible, hovered;
   1628   bool pressed = MenuButtonFrame(title, enabled, height, &visible, &hovered, &bb);
   1629   if (!visible)
   1630     return false;
   1631 
   1632   const SmallString value_text = SmallString::from_sprintf(format, *value);
   1633   const ImVec2 value_size(ImGui::CalcTextSize(value_text.c_str()));
   1634 
   1635   const float midpoint = bb.Min.y + font->FontSize + LayoutScale(4.0f);
   1636   const float text_end = bb.Max.x - value_size.x;
   1637   const ImRect title_bb(bb.Min, ImVec2(text_end, midpoint));
   1638   const ImRect summary_bb(ImVec2(bb.Min.x, midpoint), ImVec2(text_end, bb.Max.y));
   1639 
   1640   if (!enabled)
   1641     ImGui::PushStyleColor(ImGuiCol_Text, ImGui::GetColorU32(ImGuiCol_TextDisabled));
   1642 
   1643   ImGui::PushFont(font);
   1644   ImGui::RenderTextClipped(title_bb.Min, title_bb.Max, title, nullptr, nullptr, ImVec2(0.0f, 0.0f), &title_bb);
   1645   ImGui::RenderTextClipped(bb.Min, bb.Max, value_text.c_str(), nullptr, nullptr, ImVec2(1.0f, 0.5f), &bb);
   1646   ImGui::PopFont();
   1647 
   1648   if (summary)
   1649   {
   1650     ImGui::PushFont(summary_font);
   1651     ImGui::RenderTextClipped(summary_bb.Min, summary_bb.Max, summary, nullptr, nullptr, ImVec2(0.0f, 0.0f),
   1652                              &summary_bb);
   1653     ImGui::PopFont();
   1654   }
   1655 
   1656   if (!enabled)
   1657     ImGui::PopStyleColor();
   1658 
   1659   if (pressed)
   1660     ImGui::OpenPopup(title);
   1661 
   1662   bool changed = false;
   1663 
   1664   ImGui::SetNextWindowSize(LayoutScale(500.0f, 192.0f));
   1665   ImGui::SetNextWindowPos((ImGui::GetIO().DisplaySize - LayoutScale(0.0f, LAYOUT_FOOTER_HEIGHT)) * 0.5f,
   1666                           ImGuiCond_Always, ImVec2(0.5f, 0.5f));
   1667 
   1668   ImGui::PushFont(g_large_font);
   1669   ImGui::PushStyleVar(ImGuiStyleVar_WindowRounding, LayoutScale(10.0f));
   1670   ImGui::PushStyleVar(ImGuiStyleVar_FramePadding, LayoutScale(ImGuiFullscreen::LAYOUT_MENU_BUTTON_X_PADDING,
   1671                                                               ImGuiFullscreen::LAYOUT_MENU_BUTTON_Y_PADDING));
   1672   ImGui::PushStyleVar(ImGuiStyleVar_FrameBorderSize, 0.0f);
   1673   ImGui::PushStyleVar(ImGuiStyleVar_WindowPadding, LayoutScale(20.0f, 20.0f));
   1674 
   1675   if (ImGui::BeginPopupModal(title, nullptr,
   1676                              ImGuiWindowFlags_NoCollapse | ImGuiWindowFlags_NoResize | ImGuiWindowFlags_NoMove))
   1677   {
   1678     BeginMenuButtons();
   1679 
   1680     const float end = ImGui::GetCurrentWindow()->WorkRect.GetWidth();
   1681     ImGui::SetNextItemWidth(end);
   1682 
   1683     changed = ImGui::SliderFloat("##value", value, min, max, format, ImGuiSliderFlags_NoInput);
   1684 
   1685     if (MenuButtonWithoutSummary(ok_text, true, LAYOUT_MENU_BUTTON_HEIGHT_NO_SUMMARY, g_large_font, ImVec2(0.5f, 0.0f)))
   1686       ImGui::CloseCurrentPopup();
   1687     EndMenuButtons();
   1688 
   1689     ImGui::EndPopup();
   1690   }
   1691 
   1692   ImGui::PopStyleVar(4);
   1693   ImGui::PopFont();
   1694 
   1695   return changed;
   1696 }
   1697 
   1698 bool ImGuiFullscreen::MenuButtonWithValue(const char* title, const char* summary, const char* value, bool enabled,
   1699                                           float height, ImFont* font, ImFont* summary_font)
   1700 {
   1701   ImRect bb;
   1702   bool visible, hovered;
   1703   bool pressed = MenuButtonFrame(title, enabled, height, &visible, &hovered, &bb);
   1704   if (!visible)
   1705     return false;
   1706 
   1707   const ImVec2 value_size(ImGui::CalcTextSize(value));
   1708 
   1709   const float midpoint = bb.Min.y + font->FontSize + LayoutScale(4.0f);
   1710   const float text_end = bb.Max.x - value_size.x;
   1711   const ImRect title_bb(bb.Min, ImVec2(text_end, midpoint));
   1712   const ImRect summary_bb(ImVec2(bb.Min.x, midpoint), ImVec2(text_end, bb.Max.y));
   1713 
   1714   if (!enabled)
   1715     ImGui::PushStyleColor(ImGuiCol_Text, ImGui::GetColorU32(ImGuiCol_TextDisabled));
   1716 
   1717   ImGui::PushFont(font);
   1718   ImGui::RenderTextClipped(title_bb.Min, title_bb.Max, title, nullptr, nullptr, ImVec2(0.0f, 0.0f), &title_bb);
   1719   ImGui::RenderTextClipped(bb.Min, bb.Max, value, nullptr, nullptr, ImVec2(1.0f, 0.5f), &bb);
   1720   ImGui::PopFont();
   1721 
   1722   if (summary)
   1723   {
   1724     ImGui::PushFont(summary_font);
   1725     ImGui::RenderTextClipped(summary_bb.Min, summary_bb.Max, summary, nullptr, nullptr, ImVec2(0.0f, 0.0f),
   1726                              &summary_bb);
   1727     ImGui::PopFont();
   1728   }
   1729 
   1730   if (!enabled)
   1731     ImGui::PopStyleColor();
   1732 
   1733   return pressed;
   1734 }
   1735 
   1736 bool ImGuiFullscreen::EnumChoiceButtonImpl(const char* title, const char* summary, s32* value_pointer,
   1737                                            const char* (*to_display_name_function)(s32 value, void* opaque),
   1738                                            void* opaque, u32 count, bool enabled, float height, ImFont* font,
   1739                                            ImFont* summary_font)
   1740 {
   1741   const bool pressed = MenuButtonWithValue(title, summary, to_display_name_function(*value_pointer, opaque), enabled,
   1742                                            height, font, summary_font);
   1743 
   1744   if (pressed)
   1745   {
   1746     s_enum_choice_button_id = ImGui::GetID(title);
   1747     s_enum_choice_button_value = *value_pointer;
   1748     s_enum_choice_button_set = false;
   1749 
   1750     ChoiceDialogOptions options;
   1751     options.reserve(count);
   1752     for (u32 i = 0; i < count; i++)
   1753       options.emplace_back(to_display_name_function(static_cast<s32>(i), opaque),
   1754                            static_cast<u32>(*value_pointer) == i);
   1755     OpenChoiceDialog(title, false, std::move(options), [](s32 index, const std::string& title, bool checked) {
   1756       if (index >= 0)
   1757         s_enum_choice_button_value = index;
   1758 
   1759       s_enum_choice_button_set = true;
   1760       CloseChoiceDialog();
   1761     });
   1762   }
   1763 
   1764   bool changed = false;
   1765   if (s_enum_choice_button_set && s_enum_choice_button_id == ImGui::GetID(title))
   1766   {
   1767     changed = s_enum_choice_button_value != *value_pointer;
   1768     if (changed)
   1769       *value_pointer = s_enum_choice_button_value;
   1770 
   1771     s_enum_choice_button_id = 0;
   1772     s_enum_choice_button_value = 0;
   1773     s_enum_choice_button_set = false;
   1774   }
   1775 
   1776   return changed;
   1777 }
   1778 
   1779 void ImGuiFullscreen::DrawShadowedText(ImDrawList* dl, ImFont* font, const ImVec2& pos, u32 col, const char* text,
   1780                                        const char* text_end /*= nullptr*/, float wrap_width /*= 0.0f*/)
   1781 {
   1782   dl->AddText(font, font->FontSize, pos + LayoutScale(1.0f, 1.0f),
   1783               s_light_theme ? IM_COL32(255, 255, 255, 100) : IM_COL32(0, 0, 0, 100), text, text_end, wrap_width);
   1784   dl->AddText(font, font->FontSize, pos, col, text, text_end, wrap_width);
   1785 }
   1786 
   1787 void ImGuiFullscreen::BeginNavBar(float x_padding /*= LAYOUT_MENU_BUTTON_X_PADDING*/,
   1788                                   float y_padding /*= LAYOUT_MENU_BUTTON_Y_PADDING*/)
   1789 {
   1790   s_menu_button_index = 0;
   1791 
   1792   ImGui::PushStyleVar(ImGuiStyleVar_FramePadding, LayoutScale(x_padding, y_padding));
   1793   ImGui::PushStyleVar(ImGuiStyleVar_FrameRounding, 0.0f);
   1794   ImGui::PushStyleVar(ImGuiStyleVar_FrameBorderSize, LayoutScale(1.0f));
   1795   ImGui::PushStyleVar(ImGuiStyleVar_ItemSpacing, LayoutScale(1.0f, 0.0f));
   1796   PushPrimaryColor();
   1797 }
   1798 
   1799 void ImGuiFullscreen::EndNavBar()
   1800 {
   1801   PopPrimaryColor();
   1802   ImGui::PopStyleVar(4);
   1803 }
   1804 
   1805 void ImGuiFullscreen::NavTitle(const char* title, float height /*= LAYOUT_MENU_BUTTON_HEIGHT_NO_SUMMARY*/,
   1806                                ImFont* font /*= g_large_font*/)
   1807 {
   1808   ImGuiWindow* window = ImGui::GetCurrentWindow();
   1809   if (window->SkipItems)
   1810     return;
   1811 
   1812   s_menu_button_index++;
   1813 
   1814   const ImVec2 text_size(font->CalcTextSizeA(font->FontSize, std::numeric_limits<float>::max(), 0.0f, title));
   1815   const ImVec2 pos(window->DC.CursorPos);
   1816   const ImGuiStyle& style = ImGui::GetStyle();
   1817   const ImVec2 size = ImVec2(text_size.x, LayoutScale(height) + style.FramePadding.y * 2.0f);
   1818 
   1819   ImGui::ItemSize(
   1820     ImVec2(size.x + style.FrameBorderSize + style.ItemSpacing.x, size.y + style.FrameBorderSize + style.ItemSpacing.y));
   1821   ImGui::SameLine();
   1822 
   1823   ImRect bb(pos, pos + size);
   1824   if (ImGui::IsClippedEx(bb, 0))
   1825     return;
   1826 
   1827   bb.Min.y += style.FramePadding.y;
   1828   bb.Max.y -= style.FramePadding.y;
   1829 
   1830   ImGui::PushFont(font);
   1831   ImGui::RenderTextClipped(bb.Min, bb.Max, title, nullptr, nullptr, ImVec2(0.0f, 0.0f), &bb);
   1832   ImGui::PopFont();
   1833 }
   1834 
   1835 void ImGuiFullscreen::RightAlignNavButtons(u32 num_items /*= 0*/,
   1836                                            float item_width /*= LAYOUT_MENU_BUTTON_HEIGHT_NO_SUMMARY*/,
   1837                                            float item_height /*= LAYOUT_MENU_BUTTON_HEIGHT_NO_SUMMARY*/)
   1838 {
   1839   ImGuiWindow* window = ImGui::GetCurrentWindow();
   1840   const ImGuiStyle& style = ImGui::GetStyle();
   1841 
   1842   const float total_item_width =
   1843     style.FramePadding.x * 2.0f + style.FrameBorderSize + style.ItemSpacing.x + LayoutScale(item_width);
   1844   const float margin = total_item_width * static_cast<float>(num_items);
   1845   ImGui::SetCursorPosX(window->InnerClipRect.Max.x - margin - style.FramePadding.x);
   1846 }
   1847 
   1848 bool ImGuiFullscreen::NavButton(const char* title, bool is_active, bool enabled /* = true */, float width /* = -1.0f */,
   1849                                 float height /* = LAYOUT_MENU_BUTTON_HEIGHT_NO_SUMMARY */,
   1850                                 ImFont* font /* = g_large_font */)
   1851 {
   1852   ImGuiWindow* window = ImGui::GetCurrentWindow();
   1853   if (window->SkipItems)
   1854     return false;
   1855 
   1856   s_menu_button_index++;
   1857 
   1858   const ImVec2 text_size(font->CalcTextSizeA(font->FontSize, std::numeric_limits<float>::max(), 0.0f, title));
   1859   const ImVec2 pos(window->DC.CursorPos);
   1860   const ImGuiStyle& style = ImGui::GetStyle();
   1861   const ImVec2 size = ImVec2(((width < 0.0f) ? text_size.x : LayoutScale(width)) + style.FramePadding.x * 2.0f,
   1862                              LayoutScale(height) + style.FramePadding.y * 2.0f);
   1863 
   1864   ImGui::ItemSize(
   1865     ImVec2(size.x + style.FrameBorderSize + style.ItemSpacing.x, size.y + style.FrameBorderSize + style.ItemSpacing.y));
   1866   ImGui::SameLine();
   1867 
   1868   ImRect bb(pos, pos + size);
   1869   const ImGuiID id = window->GetID(title);
   1870   if (enabled)
   1871   {
   1872     // bit contradictory - we don't want this button to be used for *gamepad* navigation, since they're usually
   1873     // activated with the bumpers and/or the back button.
   1874     if (!ImGui::ItemAdd(bb, id, nullptr, ImGuiItemFlags_NoNav | ImGuiItemFlags_NoNavDefaultFocus))
   1875       return false;
   1876   }
   1877   else
   1878   {
   1879     if (ImGui::IsClippedEx(bb, id))
   1880       return false;
   1881   }
   1882 
   1883   bool held;
   1884   bool pressed;
   1885   bool hovered;
   1886   if (enabled)
   1887   {
   1888     pressed = ImGui::ButtonBehavior(bb, id, &hovered, &held, ImGuiButtonFlags_NoNavFocus);
   1889     if (hovered)
   1890     {
   1891       const ImU32 col = ImGui::GetColorU32(held ? ImGuiCol_ButtonActive : ImGuiCol_ButtonHovered, 1.0f);
   1892       DrawMenuButtonFrame(bb.Min, bb.Max, col, true, 0.0f);
   1893     }
   1894   }
   1895   else
   1896   {
   1897     pressed = false;
   1898     held = false;
   1899     hovered = false;
   1900   }
   1901 
   1902   bb.Min += style.FramePadding;
   1903   bb.Max -= style.FramePadding;
   1904 
   1905   ImGui::PushStyleColor(
   1906     ImGuiCol_Text,
   1907     ImGui::GetColorU32(enabled ? (is_active ? ImGuiCol_Text : ImGuiCol_TextDisabled) : ImGuiCol_ButtonHovered));
   1908 
   1909   ImGui::PushFont(font);
   1910   ImGui::RenderTextClipped(bb.Min, bb.Max, title, nullptr, nullptr, ImVec2(0.0f, 0.0f), &bb);
   1911   ImGui::PopFont();
   1912 
   1913   ImGui::PopStyleColor();
   1914 
   1915   return pressed;
   1916 }
   1917 
   1918 bool ImGuiFullscreen::NavTab(const char* title, bool is_active, bool enabled /* = true */, float width, float height,
   1919                              const ImVec4& background, ImFont* font /* = g_large_font */)
   1920 {
   1921   ImGuiWindow* window = ImGui::GetCurrentWindow();
   1922   if (window->SkipItems)
   1923     return false;
   1924 
   1925   s_menu_button_index++;
   1926 
   1927   const ImVec2 text_size(font->CalcTextSizeA(font->FontSize, std::numeric_limits<float>::max(), 0.0f, title));
   1928   const ImVec2 pos(window->DC.CursorPos);
   1929   const ImVec2 size = ImVec2(((width < 0.0f) ? text_size.x : LayoutScale(width)), LayoutScale(height));
   1930 
   1931   ImGui::PushStyleVar(ImGuiStyleVar_ItemSpacing, ImVec2(0.0f, 0.0f));
   1932   ImGui::ItemSize(ImVec2(size.x, size.y));
   1933   ImGui::SameLine();
   1934   ImGui::PopStyleVar();
   1935 
   1936   ImRect bb(pos, pos + size);
   1937   const ImGuiID id = window->GetID(title);
   1938   if (enabled)
   1939   {
   1940     // bit contradictory - we don't want this button to be used for *gamepad* navigation, since they're usually
   1941     // activated with the bumpers and/or the back button.
   1942     if (!ImGui::ItemAdd(bb, id, nullptr, ImGuiItemFlags_NoNav | ImGuiItemFlags_NoNavDefaultFocus))
   1943       return false;
   1944   }
   1945   else
   1946   {
   1947     if (ImGui::IsClippedEx(bb, id))
   1948       return false;
   1949   }
   1950 
   1951   bool held;
   1952   bool pressed;
   1953   bool hovered;
   1954   if (enabled)
   1955   {
   1956     pressed = ImGui::ButtonBehavior(bb, id, &hovered, &held, ImGuiButtonFlags_NoNavFocus);
   1957   }
   1958   else
   1959   {
   1960     pressed = false;
   1961     held = false;
   1962     hovered = false;
   1963   }
   1964 
   1965   const ImU32 col =
   1966     hovered ? ImGui::GetColorU32(held ? ImGuiCol_ButtonActive : ImGuiCol_ButtonHovered, 1.0f) :
   1967               ImGui::GetColorU32(is_active ? background : ImVec4(background.x, background.y, background.z, 0.5f));
   1968 
   1969   if (hovered)
   1970     DrawMenuButtonFrame(bb.Min, bb.Max, col, true, 0.0f);
   1971 
   1972   if (is_active)
   1973   {
   1974     const float line_thickness = LayoutScale(2.0f);
   1975     ImGui::GetWindowDrawList()->AddLine(ImVec2(bb.Min.x, bb.Max.y - line_thickness),
   1976                                         ImVec2(bb.Max.x, bb.Max.y - line_thickness),
   1977                                         ImGui::GetColorU32(ImGuiCol_TextDisabled), line_thickness);
   1978   }
   1979 
   1980   const ImVec2 pad(std::max((size.x - text_size.x) * 0.5f, 0.0f), std::max((size.y - text_size.y) * 0.5f, 0.0f));
   1981   bb.Min += pad;
   1982   bb.Max -= pad;
   1983 
   1984   ImGui::PushStyleColor(
   1985     ImGuiCol_Text,
   1986     ImGui::GetColorU32(enabled ? (is_active ? ImGuiCol_Text : ImGuiCol_TextDisabled) : ImGuiCol_ButtonHovered));
   1987 
   1988   ImGui::PushFont(font);
   1989   ImGui::RenderTextClipped(bb.Min, bb.Max, title, nullptr, nullptr, ImVec2(0.0f, 0.0f), &bb);
   1990   ImGui::PopFont();
   1991 
   1992   ImGui::PopStyleColor();
   1993 
   1994   return pressed;
   1995 }
   1996 
   1997 bool ImGuiFullscreen::BeginHorizontalMenu(const char* name, const ImVec2& position, const ImVec2& size, u32 num_items)
   1998 {
   1999   s_menu_button_index = 0;
   2000 
   2001   const float item_padding = LayoutScale(LAYOUT_HORIZONTAL_MENU_PADDING);
   2002   const float item_width = LayoutScale(LAYOUT_HORIZONTAL_MENU_ITEM_WIDTH);
   2003   const float item_spacing = LayoutScale(30.0f);
   2004   const float menu_width = static_cast<float>(num_items) * (item_width + item_spacing) - item_spacing;
   2005   const float menu_height = LayoutScale(LAYOUT_HORIZONTAL_MENU_HEIGHT);
   2006 
   2007   ImGui::PushStyleVar(ImGuiStyleVar_FramePadding, ImVec2(item_padding, item_padding));
   2008   ImGui::PushStyleVar(ImGuiStyleVar_FrameRounding, 0.0f);
   2009   ImGui::PushStyleVar(ImGuiStyleVar_FrameBorderSize, LayoutScale(1.0f));
   2010   ImGui::PushStyleVar(ImGuiStyleVar_ItemSpacing, ImVec2(item_spacing, 0.0f));
   2011 
   2012   if (!BeginFullscreenWindow(position, size, name, UIBackgroundColor, 0.0f, ImVec2()))
   2013     return false;
   2014 
   2015   ImGui::SetCursorPos(ImVec2((size.x - menu_width) * 0.5f, (size.y - menu_height) * 0.5f));
   2016 
   2017   PrerenderMenuButtonBorder();
   2018   return true;
   2019 }
   2020 
   2021 void ImGuiFullscreen::EndHorizontalMenu()
   2022 {
   2023   ImGui::PopStyleVar(4);
   2024   EndFullscreenWindow();
   2025 }
   2026 
   2027 bool ImGuiFullscreen::HorizontalMenuItem(GPUTexture* icon, const char* title, const char* description)
   2028 {
   2029   ImGuiWindow* window = ImGui::GetCurrentWindow();
   2030   if (window->SkipItems)
   2031     return false;
   2032 
   2033   const ImVec2 pos = window->DC.CursorPos;
   2034   const ImVec2 size = LayoutScale(LAYOUT_HORIZONTAL_MENU_ITEM_WIDTH, LAYOUT_HORIZONTAL_MENU_HEIGHT);
   2035   ImRect bb = ImRect(pos, pos + size);
   2036 
   2037   const ImGuiID id = window->GetID(title);
   2038   ImGui::ItemSize(size);
   2039   if (!ImGui::ItemAdd(bb, id))
   2040     return false;
   2041 
   2042   bool held;
   2043   bool hovered;
   2044   const bool pressed = ImGui::ButtonBehavior(bb, id, &hovered, &held, 0);
   2045   if (hovered)
   2046   {
   2047     const ImU32 col = ImGui::GetColorU32(held ? ImGuiCol_ButtonActive : ImGuiCol_ButtonHovered, 1.0f);
   2048 
   2049     const float t = static_cast<float>(std::min(std::abs(std::sin(ImGui::GetTime() * 0.75) * 1.1), 1.0));
   2050     ImGui::PushStyleColor(ImGuiCol_Border, ImGui::GetColorU32(ImGuiCol_Border, t));
   2051 
   2052     DrawMenuButtonFrame(bb.Min, bb.Max, col, true, 0.0f);
   2053 
   2054     ImGui::PopStyleColor();
   2055   }
   2056 
   2057   const ImGuiStyle& style = ImGui::GetStyle();
   2058   bb.Min += style.FramePadding;
   2059   bb.Max -= style.FramePadding;
   2060 
   2061   const float avail_width = bb.Max.x - bb.Min.x;
   2062   const float icon_size = LayoutScale(150.0f);
   2063   const ImVec2 icon_pos = bb.Min + ImVec2((avail_width - icon_size) * 0.5f, 0.0f);
   2064 
   2065   ImDrawList* dl = ImGui::GetWindowDrawList();
   2066   dl->AddImage(reinterpret_cast<ImTextureID>(icon), icon_pos, icon_pos + ImVec2(icon_size, icon_size));
   2067 
   2068   ImFont* title_font = g_large_font;
   2069   const ImVec2 title_size = title_font->CalcTextSizeA(title_font->FontSize, avail_width, 0.0f, title);
   2070   const ImVec2 title_pos =
   2071     ImVec2(bb.Min.x + (avail_width - title_size.x) * 0.5f, icon_pos.y + icon_size + LayoutScale(10.0f));
   2072   const ImVec4 title_bb = ImVec4(title_pos.x, title_pos.y, title_pos.x + title_size.x, title_pos.y + title_size.y);
   2073 
   2074   dl->AddText(title_font, title_font->FontSize, title_pos, ImGui::GetColorU32(ImGuiCol_Text), title, nullptr, 0.0f,
   2075               &title_bb);
   2076 
   2077   ImFont* desc_font = g_medium_font;
   2078   const ImVec2 desc_size = desc_font->CalcTextSizeA(desc_font->FontSize, avail_width, avail_width, description);
   2079   const ImVec2 desc_pos = ImVec2(bb.Min.x + (avail_width - desc_size.x) * 0.5f, title_bb.w + LayoutScale(10.0f));
   2080   const ImVec4 desc_bb = ImVec4(desc_pos.x, desc_pos.y, desc_pos.x + desc_size.x, desc_pos.y + desc_size.y);
   2081 
   2082   dl->AddText(desc_font, desc_font->FontSize, desc_pos, ImGui::GetColorU32(ImGuiCol_Text), description, nullptr,
   2083               avail_width, &desc_bb);
   2084 
   2085   ImGui::SameLine();
   2086 
   2087   s_menu_button_index++;
   2088   return pressed;
   2089 }
   2090 
   2091 void ImGuiFullscreen::PopulateFileSelectorItems()
   2092 {
   2093   s_file_selector_items.clear();
   2094 
   2095   if (s_file_selector_current_directory.empty())
   2096   {
   2097     for (std::string& root_path : FileSystem::GetRootDirectoryList())
   2098       s_file_selector_items.emplace_back(fmt::format(ICON_FA_FOLDER " {}", root_path), std::move(root_path), false);
   2099   }
   2100   else
   2101   {
   2102     FileSystem::FindResultsArray results;
   2103     FileSystem::FindFiles(s_file_selector_current_directory.c_str(), "*",
   2104                           FILESYSTEM_FIND_FILES | FILESYSTEM_FIND_FOLDERS | FILESYSTEM_FIND_HIDDEN_FILES |
   2105                             FILESYSTEM_FIND_RELATIVE_PATHS | FILESYSTEM_FIND_SORT_BY_NAME,
   2106                           &results);
   2107 
   2108     std::string parent_path;
   2109     std::string::size_type sep_pos = s_file_selector_current_directory.rfind(FS_OSPATH_SEPARATOR_CHARACTER);
   2110     if (sep_pos != std::string::npos)
   2111       parent_path = Path::Canonicalize(s_file_selector_current_directory.substr(0, sep_pos));
   2112 
   2113     s_file_selector_items.emplace_back(ICON_FA_FOLDER_OPEN "  <Parent Directory>", std::move(parent_path), false);
   2114 
   2115     for (const FILESYSTEM_FIND_DATA& fd : results)
   2116     {
   2117       std::string full_path =
   2118         fmt::format("{}" FS_OSPATH_SEPARATOR_STR "{}", s_file_selector_current_directory, fd.FileName);
   2119 
   2120       if (fd.Attributes & FILESYSTEM_FILE_ATTRIBUTE_DIRECTORY)
   2121       {
   2122         std::string title = fmt::format(ICON_FA_FOLDER " {}", fd.FileName);
   2123         s_file_selector_items.emplace_back(std::move(title), std::move(full_path), false);
   2124       }
   2125       else
   2126       {
   2127         if (s_file_selector_filters.empty() ||
   2128             std::none_of(s_file_selector_filters.begin(), s_file_selector_filters.end(),
   2129                          [&fd](const std::string& filter) {
   2130                            return StringUtil::WildcardMatch(fd.FileName.c_str(), filter.c_str(), false);
   2131                          }))
   2132         {
   2133           continue;
   2134         }
   2135 
   2136         std::string title = fmt::format(ICON_FA_FILE " {}", fd.FileName);
   2137         s_file_selector_items.emplace_back(std::move(title), std::move(full_path), true);
   2138       }
   2139     }
   2140   }
   2141 }
   2142 
   2143 void ImGuiFullscreen::SetFileSelectorDirectory(std::string dir)
   2144 {
   2145   while (!dir.empty() && dir.back() == FS_OSPATH_SEPARATOR_CHARACTER)
   2146     dir.erase(dir.size() - 1);
   2147 
   2148   s_file_selector_current_directory = std::move(dir);
   2149   PopulateFileSelectorItems();
   2150 }
   2151 
   2152 bool ImGuiFullscreen::IsFileSelectorOpen()
   2153 {
   2154   return s_file_selector_open;
   2155 }
   2156 
   2157 void ImGuiFullscreen::OpenFileSelector(std::string_view title, bool select_directory, FileSelectorCallback callback,
   2158                                        FileSelectorFilters filters, std::string initial_directory)
   2159 {
   2160   if (initial_directory.empty() || !FileSystem::DirectoryExists(initial_directory.c_str()))
   2161     initial_directory = FileSystem::GetWorkingDirectory();
   2162 
   2163   if (Host::ShouldPreferHostFileSelector())
   2164   {
   2165     Host::OpenHostFileSelectorAsync(ImGuiManager::StripIconCharacters(title), select_directory, std::move(callback),
   2166                                     std::move(filters), initial_directory);
   2167     return;
   2168   }
   2169 
   2170   if (s_file_selector_open)
   2171     CloseFileSelector();
   2172 
   2173   s_file_selector_open = true;
   2174   s_file_selector_directory = select_directory;
   2175   s_file_selector_title = fmt::format("{}##file_selector", title);
   2176   s_file_selector_callback = std::move(callback);
   2177   s_file_selector_filters = std::move(filters);
   2178 
   2179   SetFileSelectorDirectory(std::move(initial_directory));
   2180   QueueResetFocus(FocusResetType::PopupOpened);
   2181 }
   2182 
   2183 void ImGuiFullscreen::CloseFileSelector()
   2184 {
   2185   if (!s_file_selector_open)
   2186     return;
   2187 
   2188   if (ImGui::IsPopupOpen(s_file_selector_title.c_str(), 0))
   2189     ImGui::ClosePopupToLevel(GImGui->OpenPopupStack.Size - 1, true);
   2190 
   2191   s_file_selector_open = false;
   2192   s_file_selector_directory = false;
   2193   std::string().swap(s_file_selector_title);
   2194   FileSelectorCallback().swap(s_file_selector_callback);
   2195   FileSelectorFilters().swap(s_file_selector_filters);
   2196   std::string().swap(s_file_selector_current_directory);
   2197   s_file_selector_items.clear();
   2198   ImGui::CloseCurrentPopup();
   2199   QueueResetFocus(FocusResetType::PopupClosed);
   2200 }
   2201 
   2202 void ImGuiFullscreen::DrawFileSelector()
   2203 {
   2204   if (!s_file_selector_open)
   2205     return;
   2206 
   2207   ImGui::SetNextWindowSize(LayoutScale(1000.0f, 650.0f));
   2208   ImGui::SetNextWindowPos((ImGui::GetIO().DisplaySize - LayoutScale(0.0f, LAYOUT_FOOTER_HEIGHT)) * 0.5f,
   2209                           ImGuiCond_Always, ImVec2(0.5f, 0.5f));
   2210   ImGui::OpenPopup(s_file_selector_title.c_str());
   2211 
   2212   FileSelectorItem* selected = nullptr;
   2213 
   2214   ImGui::PushFont(g_large_font);
   2215   ImGui::PushStyleVar(ImGuiStyleVar_WindowRounding, LayoutScale(10.0f));
   2216   ImGui::PushStyleVar(ImGuiStyleVar_FramePadding,
   2217                       LayoutScale(LAYOUT_MENU_BUTTON_X_PADDING, LAYOUT_MENU_BUTTON_Y_PADDING));
   2218   ImGui::PushStyleVar(ImGuiStyleVar_FrameBorderSize, 0.0f);
   2219   ImGui::PushStyleColor(ImGuiCol_Text, UIPrimaryTextColor);
   2220   ImGui::PushStyleColor(ImGuiCol_TitleBg, UIPrimaryDarkColor);
   2221   ImGui::PushStyleColor(ImGuiCol_TitleBgActive, UIPrimaryColor);
   2222 
   2223   bool is_open = !WantsToCloseMenu();
   2224   bool directory_selected = false;
   2225   if (ImGui::BeginPopupModal(s_file_selector_title.c_str(), &is_open,
   2226                              ImGuiWindowFlags_NoCollapse | ImGuiWindowFlags_NoResize | ImGuiWindowFlags_NoMove))
   2227   {
   2228     ImGui::PushStyleColor(ImGuiCol_Text, UIBackgroundTextColor);
   2229 
   2230     ResetFocusHere();
   2231     BeginMenuButtons();
   2232 
   2233     if (!s_file_selector_current_directory.empty())
   2234     {
   2235       MenuButton(SmallString::from_format(ICON_FA_FOLDER_OPEN " {}", s_file_selector_current_directory).c_str(),
   2236                  nullptr, false, LAYOUT_MENU_BUTTON_HEIGHT_NO_SUMMARY);
   2237     }
   2238 
   2239     if (s_file_selector_directory && !s_file_selector_current_directory.empty())
   2240     {
   2241       if (MenuButton(ICON_FA_FOLDER_PLUS " <Use This Directory>", nullptr, true, LAYOUT_MENU_BUTTON_HEIGHT_NO_SUMMARY))
   2242         directory_selected = true;
   2243     }
   2244 
   2245     for (FileSelectorItem& item : s_file_selector_items)
   2246     {
   2247       if (MenuButton(item.display_name.c_str(), nullptr, true, LAYOUT_MENU_BUTTON_HEIGHT_NO_SUMMARY))
   2248         selected = &item;
   2249     }
   2250 
   2251     EndMenuButtons();
   2252 
   2253     ImGui::PopStyleColor(1);
   2254 
   2255     ImGui::EndPopup();
   2256   }
   2257   else
   2258   {
   2259     is_open = false;
   2260   }
   2261 
   2262   ImGui::PopStyleColor(3);
   2263   ImGui::PopStyleVar(3);
   2264   ImGui::PopFont();
   2265 
   2266   if (is_open)
   2267     GetFileSelectorHelpText(s_fullscreen_footer_text);
   2268 
   2269   if (selected)
   2270   {
   2271     if (selected->is_file)
   2272     {
   2273       s_file_selector_callback(selected->full_path);
   2274     }
   2275     else
   2276     {
   2277       SetFileSelectorDirectory(std::move(selected->full_path));
   2278       QueueResetFocus(FocusResetType::Other);
   2279     }
   2280   }
   2281   else if (directory_selected)
   2282   {
   2283     s_file_selector_callback(s_file_selector_current_directory);
   2284   }
   2285   else if (!is_open)
   2286   {
   2287     std::string no_path;
   2288     s_file_selector_callback(no_path);
   2289     CloseFileSelector();
   2290   }
   2291   else
   2292   {
   2293     if (ImGui::IsKeyPressed(ImGuiKey_Backspace, false) || ImGui::IsKeyPressed(ImGuiKey_NavGamepadMenu, false))
   2294     {
   2295       if (!s_file_selector_items.empty() && s_file_selector_items.front().display_name == ICON_FA_FOLDER_OPEN
   2296                                               "  <Parent Directory>")
   2297       {
   2298         SetFileSelectorDirectory(std::move(s_file_selector_items.front().full_path));
   2299         QueueResetFocus(FocusResetType::Other);
   2300       }
   2301     }
   2302   }
   2303 }
   2304 
   2305 bool ImGuiFullscreen::IsChoiceDialogOpen()
   2306 {
   2307   return s_choice_dialog_open;
   2308 }
   2309 
   2310 void ImGuiFullscreen::OpenChoiceDialog(std::string_view title, bool checkable, ChoiceDialogOptions options,
   2311                                        ChoiceDialogCallback callback)
   2312 {
   2313   if (s_choice_dialog_open)
   2314     CloseChoiceDialog();
   2315 
   2316   s_choice_dialog_open = true;
   2317   s_choice_dialog_checkable = checkable;
   2318   s_choice_dialog_title = fmt::format("{}##choice_dialog", title);
   2319   s_choice_dialog_options = std::move(options);
   2320   s_choice_dialog_callback = std::move(callback);
   2321   QueueResetFocus(FocusResetType::PopupOpened);
   2322 }
   2323 
   2324 void ImGuiFullscreen::CloseChoiceDialog()
   2325 {
   2326   if (!s_choice_dialog_open)
   2327     return;
   2328 
   2329   if (ImGui::IsPopupOpen(s_choice_dialog_title.c_str(), 0))
   2330     ImGui::ClosePopupToLevel(GImGui->OpenPopupStack.Size - 1, true);
   2331 
   2332   s_choice_dialog_open = false;
   2333   s_choice_dialog_checkable = false;
   2334   std::string().swap(s_choice_dialog_title);
   2335   ChoiceDialogOptions().swap(s_choice_dialog_options);
   2336   ChoiceDialogCallback().swap(s_choice_dialog_callback);
   2337   QueueResetFocus(FocusResetType::PopupClosed);
   2338 }
   2339 
   2340 void ImGuiFullscreen::DrawChoiceDialog()
   2341 {
   2342   if (!s_choice_dialog_open)
   2343     return;
   2344 
   2345   ImGui::PushFont(g_large_font);
   2346   ImGui::PushStyleVar(ImGuiStyleVar_WindowRounding, LayoutScale(10.0f));
   2347   ImGui::PushStyleVar(ImGuiStyleVar_FramePadding,
   2348                       LayoutScale(LAYOUT_MENU_BUTTON_X_PADDING, LAYOUT_MENU_BUTTON_Y_PADDING));
   2349   ImGui::PushStyleVar(ImGuiStyleVar_FrameBorderSize, 0.0f);
   2350   ImGui::PushStyleColor(ImGuiCol_Text, UIPrimaryTextColor);
   2351   ImGui::PushStyleColor(ImGuiCol_TitleBg, UIPrimaryDarkColor);
   2352   ImGui::PushStyleColor(ImGuiCol_TitleBgActive, UIPrimaryColor);
   2353 
   2354   const float width = LayoutScale(600.0f);
   2355   const float title_height =
   2356     g_large_font->FontSize + ImGui::GetStyle().FramePadding.y * 2.0f + ImGui::GetStyle().WindowPadding.y * 2.0f;
   2357   const float height =
   2358     std::min(LayoutScale(480.0f), title_height + (LayoutScale(LAYOUT_MENU_BUTTON_HEIGHT_NO_SUMMARY) +
   2359                                                   LayoutScale(LAYOUT_MENU_BUTTON_Y_PADDING) * 2.0f) *
   2360                                                    static_cast<float>(s_choice_dialog_options.size()));
   2361   ImGui::SetNextWindowSize(ImVec2(width, height));
   2362   ImGui::SetNextWindowPos((ImGui::GetIO().DisplaySize - LayoutScale(0.0f, LAYOUT_FOOTER_HEIGHT)) * 0.5f,
   2363                           ImGuiCond_Always, ImVec2(0.5f, 0.5f));
   2364   ImGui::OpenPopup(s_choice_dialog_title.c_str());
   2365 
   2366   bool is_open = true;
   2367   s32 choice = -1;
   2368 
   2369   if (ImGui::BeginPopupModal(s_choice_dialog_title.c_str(), &is_open,
   2370                              ImGuiWindowFlags_NoCollapse | ImGuiWindowFlags_NoResize | ImGuiWindowFlags_NoMove))
   2371   {
   2372     ImGui::PushStyleColor(ImGuiCol_Text, UIBackgroundTextColor);
   2373 
   2374     ResetFocusHere();
   2375     BeginMenuButtons();
   2376 
   2377     if (s_choice_dialog_checkable)
   2378     {
   2379       for (s32 i = 0; i < static_cast<s32>(s_choice_dialog_options.size()); i++)
   2380       {
   2381         auto& option = s_choice_dialog_options[i];
   2382 
   2383         const SmallString title =
   2384           SmallString::from_format("{0} {1}", option.second ? ICON_FA_CHECK_SQUARE : ICON_FA_SQUARE, option.first);
   2385         if (MenuButton(title.c_str(), nullptr, true, LAYOUT_MENU_BUTTON_HEIGHT_NO_SUMMARY))
   2386         {
   2387           choice = i;
   2388           option.second = !option.second;
   2389         }
   2390       }
   2391     }
   2392     else
   2393     {
   2394       for (s32 i = 0; i < static_cast<s32>(s_choice_dialog_options.size()); i++)
   2395       {
   2396         auto& option = s_choice_dialog_options[i];
   2397         if (ActiveButtonWithRightText(option.first.c_str(), option.second ? ICON_FA_CHECK : nullptr, option.second,
   2398                                       true, LAYOUT_MENU_BUTTON_HEIGHT_NO_SUMMARY))
   2399         {
   2400           choice = i;
   2401           for (s32 j = 0; j < static_cast<s32>(s_choice_dialog_options.size()); j++)
   2402             s_choice_dialog_options[j].second = (j == i);
   2403         }
   2404       }
   2405     }
   2406 
   2407     EndMenuButtons();
   2408 
   2409     ImGui::PopStyleColor(1);
   2410 
   2411     ImGui::EndPopup();
   2412   }
   2413 
   2414   ImGui::PopStyleColor(3);
   2415   ImGui::PopStyleVar(3);
   2416   ImGui::PopFont();
   2417 
   2418   is_open &= !WantsToCloseMenu();
   2419 
   2420   if (choice >= 0)
   2421   {
   2422     const auto& option = s_choice_dialog_options[choice];
   2423     s_choice_dialog_callback(choice, option.first, option.second);
   2424   }
   2425   else if (!is_open)
   2426   {
   2427     std::string no_string;
   2428     s_choice_dialog_callback(-1, no_string, false);
   2429     CloseChoiceDialog();
   2430   }
   2431   else
   2432   {
   2433     GetChoiceDialogHelpText(s_fullscreen_footer_text);
   2434   }
   2435 }
   2436 
   2437 bool ImGuiFullscreen::IsInputDialogOpen()
   2438 {
   2439   return s_input_dialog_open;
   2440 }
   2441 
   2442 void ImGuiFullscreen::OpenInputStringDialog(std::string title, std::string message, std::string caption,
   2443                                             std::string ok_button_text, InputStringDialogCallback callback)
   2444 {
   2445   s_input_dialog_open = true;
   2446   s_input_dialog_title = std::move(title);
   2447   s_input_dialog_message = std::move(message);
   2448   s_input_dialog_caption = std::move(caption);
   2449   s_input_dialog_ok_text = std::move(ok_button_text);
   2450   s_input_dialog_callback = std::move(callback);
   2451   QueueResetFocus(FocusResetType::PopupOpened);
   2452 }
   2453 
   2454 void ImGuiFullscreen::DrawInputDialog()
   2455 {
   2456   if (!s_input_dialog_open)
   2457     return;
   2458 
   2459   ImGui::SetNextWindowSize(LayoutScale(700.0f, 0.0f));
   2460   ImGui::SetNextWindowPos((ImGui::GetIO().DisplaySize - LayoutScale(0.0f, LAYOUT_FOOTER_HEIGHT)) * 0.5f,
   2461                           ImGuiCond_Always, ImVec2(0.5f, 0.5f));
   2462   ImGui::OpenPopup(s_input_dialog_title.c_str());
   2463 
   2464   ImGui::PushFont(g_large_font);
   2465   ImGui::PushStyleVar(ImGuiStyleVar_WindowRounding, LayoutScale(10.0f));
   2466   ImGui::PushStyleVar(ImGuiStyleVar_FramePadding,
   2467                       LayoutScale(LAYOUT_MENU_BUTTON_X_PADDING, LAYOUT_MENU_BUTTON_Y_PADDING));
   2468   ImGui::PushStyleVar(ImGuiStyleVar_FrameBorderSize, 0.0f);
   2469   ImGui::PushStyleColor(ImGuiCol_Text, UIPrimaryTextColor);
   2470   ImGui::PushStyleColor(ImGuiCol_TitleBg, UIPrimaryDarkColor);
   2471   ImGui::PushStyleColor(ImGuiCol_TitleBgActive, UIPrimaryColor);
   2472 
   2473   bool is_open = true;
   2474   if (ImGui::BeginPopupModal(s_input_dialog_title.c_str(), &is_open,
   2475                              ImGuiWindowFlags_NoTitleBar | ImGuiWindowFlags_NoCollapse | ImGuiWindowFlags_NoResize |
   2476                                ImGuiWindowFlags_NoMove))
   2477   {
   2478     ResetFocusHere();
   2479     ImGui::TextWrapped("%s", s_input_dialog_message.c_str());
   2480 
   2481     BeginMenuButtons();
   2482 
   2483     ImGui::SetCursorPosY(ImGui::GetCursorPosY() + LayoutScale(10.0f));
   2484 
   2485     if (!s_input_dialog_caption.empty())
   2486     {
   2487       const float prev = ImGui::GetCursorPosX();
   2488       ImGui::TextUnformatted(s_input_dialog_caption.c_str());
   2489       ImGui::SetNextItemWidth(ImGui::GetCursorPosX() - prev);
   2490     }
   2491     else
   2492     {
   2493       ImGui::SetNextItemWidth(ImGui::GetCurrentWindow()->WorkRect.GetWidth());
   2494     }
   2495     ImGui::InputText("##input", &s_input_dialog_text);
   2496 
   2497     ImGui::SetCursorPosY(ImGui::GetCursorPosY() + LayoutScale(10.0f));
   2498 
   2499     const bool ok_enabled = !s_input_dialog_text.empty();
   2500 
   2501     if (ActiveButton(s_input_dialog_ok_text.c_str(), false, ok_enabled) && ok_enabled)
   2502     {
   2503       // have to move out in case they open another dialog in the callback
   2504       InputStringDialogCallback cb(std::move(s_input_dialog_callback));
   2505       std::string text(std::move(s_input_dialog_text));
   2506       CloseInputDialog();
   2507       ImGui::CloseCurrentPopup();
   2508       cb(std::move(text));
   2509     }
   2510 
   2511     if (ActiveButton(ICON_FA_TIMES " Cancel", false))
   2512     {
   2513       CloseInputDialog();
   2514 
   2515       ImGui::CloseCurrentPopup();
   2516     }
   2517 
   2518     EndMenuButtons();
   2519 
   2520     ImGui::EndPopup();
   2521   }
   2522   if (!is_open)
   2523     CloseInputDialog();
   2524   else
   2525     GetInputDialogHelpText(s_fullscreen_footer_text);
   2526 
   2527   ImGui::PopStyleColor(3);
   2528   ImGui::PopStyleVar(3);
   2529   ImGui::PopFont();
   2530 }
   2531 
   2532 void ImGuiFullscreen::CloseInputDialog()
   2533 {
   2534   if (!s_input_dialog_open)
   2535     return;
   2536 
   2537   if (ImGui::IsPopupOpen(s_input_dialog_title.c_str(), 0))
   2538     ImGui::ClosePopupToLevel(GImGui->OpenPopupStack.Size - 1, true);
   2539 
   2540   s_input_dialog_open = false;
   2541   s_input_dialog_title = {};
   2542   s_input_dialog_message = {};
   2543   s_input_dialog_caption = {};
   2544   s_input_dialog_ok_text = {};
   2545   s_input_dialog_text = {};
   2546   s_input_dialog_callback = {};
   2547 }
   2548 
   2549 bool ImGuiFullscreen::IsMessageBoxDialogOpen()
   2550 {
   2551   return s_message_dialog_open;
   2552 }
   2553 
   2554 void ImGuiFullscreen::OpenConfirmMessageDialog(std::string title, std::string message,
   2555                                                ConfirmMessageDialogCallback callback, std::string yes_button_text,
   2556                                                std::string no_button_text)
   2557 {
   2558   CloseMessageDialog();
   2559 
   2560   s_message_dialog_open = true;
   2561   s_message_dialog_title = std::move(title);
   2562   s_message_dialog_message = std::move(message);
   2563   s_message_dialog_callback = std::move(callback);
   2564   s_message_dialog_buttons[0] = std::move(yes_button_text);
   2565   s_message_dialog_buttons[1] = std::move(no_button_text);
   2566   QueueResetFocus(FocusResetType::PopupOpened);
   2567 }
   2568 
   2569 void ImGuiFullscreen::OpenInfoMessageDialog(std::string title, std::string message, InfoMessageDialogCallback callback,
   2570                                             std::string button_text)
   2571 {
   2572   CloseMessageDialog();
   2573 
   2574   s_message_dialog_open = true;
   2575   s_message_dialog_title = std::move(title);
   2576   s_message_dialog_message = std::move(message);
   2577   s_message_dialog_callback = std::move(callback);
   2578   s_message_dialog_buttons[0] = std::move(button_text);
   2579   QueueResetFocus(FocusResetType::PopupOpened);
   2580 }
   2581 
   2582 void ImGuiFullscreen::OpenMessageDialog(std::string title, std::string message, MessageDialogCallback callback,
   2583                                         std::string first_button_text, std::string second_button_text,
   2584                                         std::string third_button_text)
   2585 {
   2586   CloseMessageDialog();
   2587 
   2588   s_message_dialog_open = true;
   2589   s_message_dialog_title = std::move(title);
   2590   s_message_dialog_message = std::move(message);
   2591   s_message_dialog_callback = std::move(callback);
   2592   s_message_dialog_buttons[0] = std::move(first_button_text);
   2593   s_message_dialog_buttons[1] = std::move(second_button_text);
   2594   s_message_dialog_buttons[2] = std::move(third_button_text);
   2595   QueueResetFocus(FocusResetType::PopupOpened);
   2596 }
   2597 
   2598 void ImGuiFullscreen::CloseMessageDialog()
   2599 {
   2600   if (!s_message_dialog_open)
   2601     return;
   2602 
   2603   if (ImGui::IsPopupOpen(s_message_dialog_title.c_str(), 0))
   2604     ImGui::ClosePopupToLevel(GImGui->OpenPopupStack.Size - 1, true);
   2605 
   2606   s_message_dialog_open = false;
   2607   s_message_dialog_title = {};
   2608   s_message_dialog_message = {};
   2609   s_message_dialog_buttons = {};
   2610   s_message_dialog_callback = {};
   2611   QueueResetFocus(FocusResetType::PopupClosed);
   2612 }
   2613 
   2614 void ImGuiFullscreen::DrawMessageDialog()
   2615 {
   2616   if (!s_message_dialog_open)
   2617     return;
   2618 
   2619   const char* win_id = s_message_dialog_title.empty() ? "##messagedialog" : s_message_dialog_title.c_str();
   2620 
   2621   ImGui::SetNextWindowSize(LayoutScale(700.0f, 0.0f));
   2622   ImGui::SetNextWindowPos((ImGui::GetIO().DisplaySize - LayoutScale(0.0f, LAYOUT_FOOTER_HEIGHT)) * 0.5f,
   2623                           ImGuiCond_Always, ImVec2(0.5f, 0.5f));
   2624   ImGui::OpenPopup(win_id);
   2625 
   2626   ImGui::PushFont(g_large_font);
   2627   ImGui::PushStyleVar(ImGuiStyleVar_WindowPadding, LayoutScale(20.0f, 20.0f));
   2628   ImGui::PushStyleVar(ImGuiStyleVar_WindowRounding, LayoutScale(10.0f));
   2629   ImGui::PushStyleVar(ImGuiStyleVar_FramePadding,
   2630                       LayoutScale(LAYOUT_MENU_BUTTON_X_PADDING, LAYOUT_MENU_BUTTON_Y_PADDING));
   2631   ImGui::PushStyleVar(ImGuiStyleVar_FrameBorderSize, 0.0f);
   2632   ImGui::PushStyleColor(ImGuiCol_Text, UIPrimaryTextColor);
   2633   ImGui::PushStyleColor(ImGuiCol_TitleBg, UIPrimaryDarkColor);
   2634   ImGui::PushStyleColor(ImGuiCol_TitleBgActive, UIPrimaryColor);
   2635 
   2636   bool is_open = true;
   2637   const u32 flags = ImGuiWindowFlags_NoCollapse | ImGuiWindowFlags_NoResize | ImGuiWindowFlags_NoMove |
   2638                     (s_message_dialog_title.empty() ? ImGuiWindowFlags_NoTitleBar : 0);
   2639   std::optional<s32> result;
   2640 
   2641   if (ImGui::BeginPopupModal(win_id, &is_open, flags))
   2642   {
   2643     ResetFocusHere();
   2644     BeginMenuButtons();
   2645 
   2646     ImGui::TextWrapped("%s", s_message_dialog_message.c_str());
   2647     ImGui::SetCursorPosY(ImGui::GetCursorPosY() + LayoutScale(20.0f));
   2648 
   2649     for (s32 button_index = 0; button_index < static_cast<s32>(s_message_dialog_buttons.size()); button_index++)
   2650     {
   2651       if (!s_message_dialog_buttons[button_index].empty() &&
   2652           ActiveButton(s_message_dialog_buttons[button_index].c_str(), false))
   2653       {
   2654         result = button_index;
   2655         ImGui::CloseCurrentPopup();
   2656       }
   2657     }
   2658 
   2659     EndMenuButtons();
   2660 
   2661     ImGui::EndPopup();
   2662   }
   2663 
   2664   ImGui::PopStyleColor(3);
   2665   ImGui::PopStyleVar(4);
   2666   ImGui::PopFont();
   2667 
   2668   if (!is_open || result.has_value())
   2669   {
   2670     // have to move out in case they open another dialog in the callback
   2671     auto cb = (std::move(s_message_dialog_callback));
   2672     CloseMessageDialog();
   2673 
   2674     if (std::holds_alternative<InfoMessageDialogCallback>(cb))
   2675     {
   2676       const InfoMessageDialogCallback& func = std::get<InfoMessageDialogCallback>(cb);
   2677       if (func)
   2678         func();
   2679     }
   2680     else if (std::holds_alternative<ConfirmMessageDialogCallback>(cb))
   2681     {
   2682       const ConfirmMessageDialogCallback& func = std::get<ConfirmMessageDialogCallback>(cb);
   2683       if (func)
   2684         func(result.value_or(1) == 0);
   2685     }
   2686   }
   2687   else
   2688   {
   2689     GetChoiceDialogHelpText(s_fullscreen_footer_text);
   2690   }
   2691 }
   2692 
   2693 static float s_notification_vertical_position = 0.15f;
   2694 static float s_notification_vertical_direction = 1.0f;
   2695 
   2696 float ImGuiFullscreen::GetNotificationVerticalPosition()
   2697 {
   2698   return s_notification_vertical_position;
   2699 }
   2700 
   2701 float ImGuiFullscreen::GetNotificationVerticalDirection()
   2702 {
   2703   return s_notification_vertical_direction;
   2704 }
   2705 
   2706 void ImGuiFullscreen::SetNotificationVerticalPosition(float position, float direction)
   2707 {
   2708   s_notification_vertical_position = position;
   2709   s_notification_vertical_direction = direction;
   2710 }
   2711 
   2712 ImGuiID ImGuiFullscreen::GetBackgroundProgressID(const char* str_id)
   2713 {
   2714   return ImHashStr(str_id);
   2715 }
   2716 
   2717 void ImGuiFullscreen::OpenBackgroundProgressDialog(const char* str_id, std::string message, s32 min, s32 max, s32 value)
   2718 {
   2719   const ImGuiID id = GetBackgroundProgressID(str_id);
   2720 
   2721   std::unique_lock<std::mutex> lock(s_background_progress_lock);
   2722 
   2723 #ifdef _DEBUG
   2724   for (const BackgroundProgressDialogData& data : s_background_progress_dialogs)
   2725   {
   2726     DebugAssert(data.id != id);
   2727   }
   2728 #endif
   2729 
   2730   BackgroundProgressDialogData data;
   2731   data.id = id;
   2732   data.message = std::move(message);
   2733   data.min = min;
   2734   data.max = max;
   2735   data.value = value;
   2736   s_background_progress_dialogs.push_back(std::move(data));
   2737 }
   2738 
   2739 void ImGuiFullscreen::UpdateBackgroundProgressDialog(const char* str_id, std::string message, s32 min, s32 max,
   2740                                                      s32 value)
   2741 {
   2742   const ImGuiID id = GetBackgroundProgressID(str_id);
   2743 
   2744   std::unique_lock<std::mutex> lock(s_background_progress_lock);
   2745 
   2746   for (BackgroundProgressDialogData& data : s_background_progress_dialogs)
   2747   {
   2748     if (data.id == id)
   2749     {
   2750       data.message = std::move(message);
   2751       data.min = min;
   2752       data.max = max;
   2753       data.value = value;
   2754       return;
   2755     }
   2756   }
   2757 
   2758   Panic("Updating unknown progress entry.");
   2759 }
   2760 
   2761 void ImGuiFullscreen::CloseBackgroundProgressDialog(const char* str_id)
   2762 {
   2763   const ImGuiID id = GetBackgroundProgressID(str_id);
   2764 
   2765   std::unique_lock<std::mutex> lock(s_background_progress_lock);
   2766 
   2767   for (auto it = s_background_progress_dialogs.begin(); it != s_background_progress_dialogs.end(); ++it)
   2768   {
   2769     if (it->id == id)
   2770     {
   2771       s_background_progress_dialogs.erase(it);
   2772       return;
   2773     }
   2774   }
   2775 
   2776   Panic("Closing unknown progress entry.");
   2777 }
   2778 
   2779 void ImGuiFullscreen::DrawBackgroundProgressDialogs(ImVec2& position, float spacing)
   2780 {
   2781   std::unique_lock<std::mutex> lock(s_background_progress_lock);
   2782   if (s_background_progress_dialogs.empty())
   2783     return;
   2784 
   2785   const float window_width = LayoutScale(500.0f);
   2786   const float window_height = LayoutScale(75.0f);
   2787 
   2788   ImGui::PushStyleColor(ImGuiCol_WindowBg, UIPrimaryDarkColor);
   2789   ImGui::PushStyleColor(ImGuiCol_PlotHistogram, UISecondaryStrongColor);
   2790   ImGui::PushStyleVar(ImGuiStyleVar_PopupRounding, LayoutScale(4.0f));
   2791   ImGui::PushStyleVar(ImGuiStyleVar_PopupBorderSize, LayoutScale(1.0f));
   2792   ImGui::PushStyleVar(ImGuiStyleVar_WindowPadding, LayoutScale(10.0f, 10.0f));
   2793   ImGui::PushStyleVar(ImGuiStyleVar_ItemSpacing, LayoutScale(10.0f, 10.0f));
   2794   ImGui::PushFont(g_medium_font);
   2795 
   2796   ImDrawList* dl = ImGui::GetForegroundDrawList();
   2797 
   2798   for (const BackgroundProgressDialogData& data : s_background_progress_dialogs)
   2799   {
   2800     const float window_pos_x = position.x;
   2801     const float window_pos_y = position.y - ((s_notification_vertical_direction < 0.0f) ? window_height : 0.0f);
   2802 
   2803     dl->AddRectFilled(ImVec2(window_pos_x, window_pos_y),
   2804                       ImVec2(window_pos_x + window_width, window_pos_y + window_height),
   2805                       IM_COL32(0x11, 0x11, 0x11, 200), LayoutScale(10.0f));
   2806 
   2807     ImVec2 pos(window_pos_x + LayoutScale(10.0f), window_pos_y + LayoutScale(10.0f));
   2808     dl->AddText(g_medium_font, g_medium_font->FontSize, pos, IM_COL32(255, 255, 255, 255), data.message.c_str(),
   2809                 nullptr, 0.0f);
   2810     pos.y += g_medium_font->FontSize + LayoutScale(10.0f);
   2811 
   2812     const ImVec2 box_end(pos.x + window_width - LayoutScale(10.0f * 2.0f), pos.y + LayoutScale(25.0f));
   2813     dl->AddRectFilled(pos, box_end, ImGui::GetColorU32(UIPrimaryDarkColor));
   2814 
   2815     if (data.min != data.max)
   2816     {
   2817       const float fraction = static_cast<float>(data.value - data.min) / static_cast<float>(data.max - data.min);
   2818       dl->AddRectFilled(pos, ImVec2(pos.x + fraction * (box_end.x - pos.x), box_end.y),
   2819                         ImGui::GetColorU32(UISecondaryColor));
   2820 
   2821       const auto text = TinyString::from_format("{}%", static_cast<int>(std::round(fraction * 100.0f)));
   2822       const ImVec2 text_size(ImGui::CalcTextSize(text));
   2823       const ImVec2 text_pos(pos.x + ((box_end.x - pos.x) / 2.0f) - (text_size.x / 2.0f),
   2824                             pos.y + ((box_end.y - pos.y) / 2.0f) - (text_size.y / 2.0f));
   2825       dl->AddText(g_medium_font, g_medium_font->FontSize, text_pos, ImGui::GetColorU32(UIPrimaryTextColor),
   2826                   text.c_str(), text.end_ptr());
   2827     }
   2828     else
   2829     {
   2830       // indeterminate, so draw a scrolling bar
   2831       const float bar_width = LayoutScale(30.0f);
   2832       const float fraction = static_cast<float>(std::fmod(ImGui::GetTime(), 2.0) * 0.5);
   2833       const ImVec2 bar_start(pos.x + ImLerp(0.0f, box_end.x, fraction) - bar_width, pos.y);
   2834       const ImVec2 bar_end(std::min(bar_start.x + bar_width, box_end.x), pos.y + LayoutScale(25.0f));
   2835       dl->AddRectFilled(ImClamp(bar_start, pos, box_end), ImClamp(bar_end, pos, box_end),
   2836                         ImGui::GetColorU32(UISecondaryColor));
   2837     }
   2838 
   2839     position.y += s_notification_vertical_direction * (window_height + spacing);
   2840   }
   2841 
   2842   ImGui::PopFont();
   2843   ImGui::PopStyleVar(4);
   2844   ImGui::PopStyleColor(2);
   2845 }
   2846 
   2847 //////////////////////////////////////////////////////////////////////////
   2848 // Notifications
   2849 //////////////////////////////////////////////////////////////////////////
   2850 
   2851 void ImGuiFullscreen::AddNotification(std::string key, float duration, std::string title, std::string text,
   2852                                       std::string image_path)
   2853 {
   2854   const Common::Timer::Value current_time = Common::Timer::GetCurrentValue();
   2855 
   2856   if (!key.empty())
   2857   {
   2858     for (auto it = s_notifications.begin(); it != s_notifications.end(); ++it)
   2859     {
   2860       if (it->key == key)
   2861       {
   2862         it->duration = duration;
   2863         it->title = std::move(title);
   2864         it->text = std::move(text);
   2865         it->badge_path = std::move(image_path);
   2866 
   2867         // Don't fade it in again
   2868         const float time_passed =
   2869           static_cast<float>(Common::Timer::ConvertValueToSeconds(current_time - it->start_time));
   2870         it->start_time =
   2871           current_time - Common::Timer::ConvertSecondsToValue(std::min(time_passed, NOTIFICATION_FADE_IN_TIME));
   2872         return;
   2873       }
   2874     }
   2875   }
   2876 
   2877   Notification notif;
   2878   notif.key = std::move(key);
   2879   notif.duration = duration;
   2880   notif.title = std::move(title);
   2881   notif.text = std::move(text);
   2882   notif.badge_path = std::move(image_path);
   2883   notif.start_time = current_time;
   2884   notif.move_time = current_time;
   2885   notif.target_y = -1.0f;
   2886   notif.last_y = -1.0f;
   2887   s_notifications.push_back(std::move(notif));
   2888 }
   2889 
   2890 void ImGuiFullscreen::ClearNotifications()
   2891 {
   2892   s_notifications.clear();
   2893 }
   2894 
   2895 void ImGuiFullscreen::DrawNotifications(ImVec2& position, float spacing)
   2896 {
   2897   if (s_notifications.empty())
   2898     return;
   2899 
   2900   static constexpr float MOVE_DURATION = 0.5f;
   2901   const Common::Timer::Value current_time = Common::Timer::GetCurrentValue();
   2902 
   2903   const float horizontal_padding = ImGuiFullscreen::LayoutScale(20.0f);
   2904   const float vertical_padding = ImGuiFullscreen::LayoutScale(10.0f);
   2905   const float horizontal_spacing = ImGuiFullscreen::LayoutScale(10.0f);
   2906   const float vertical_spacing = ImGuiFullscreen::LayoutScale(4.0f);
   2907   const float badge_size = ImGuiFullscreen::LayoutScale(48.0f);
   2908   const float min_width = ImGuiFullscreen::LayoutScale(200.0f);
   2909   const float max_width = ImGuiFullscreen::LayoutScale(800.0f);
   2910   const float max_text_width = max_width - badge_size - (horizontal_padding * 2.0f) - horizontal_spacing;
   2911   const float min_height = (vertical_padding * 2.0f) + badge_size;
   2912   const float shadow_size = ImGuiFullscreen::LayoutScale(4.0f);
   2913   const float rounding = ImGuiFullscreen::LayoutScale(4.0f);
   2914 
   2915   ImFont* const title_font = ImGuiFullscreen::g_large_font;
   2916   ImFont* const text_font = ImGuiFullscreen::g_medium_font;
   2917 
   2918   const u32 toast_background_color = s_light_theme ? IM_COL32(241, 241, 241, 255) : IM_COL32(0x21, 0x21, 0x21, 255);
   2919   const u32 toast_border_color = s_light_theme ? IM_COL32(0x88, 0x88, 0x88, 255) : IM_COL32(0x48, 0x48, 0x48, 255);
   2920   const u32 toast_title_color = s_light_theme ? IM_COL32(1, 1, 1, 255) : IM_COL32(0xff, 0xff, 0xff, 255);
   2921   const u32 toast_text_color = s_light_theme ? IM_COL32(0, 0, 0, 255) : IM_COL32(0xff, 0xff, 0xff, 255);
   2922 
   2923   for (u32 index = 0; index < static_cast<u32>(s_notifications.size());)
   2924   {
   2925     Notification& notif = s_notifications[index];
   2926     const float time_passed = static_cast<float>(Common::Timer::ConvertValueToSeconds(current_time - notif.start_time));
   2927     if (time_passed >= notif.duration)
   2928     {
   2929       s_notifications.erase(s_notifications.begin() + index);
   2930       continue;
   2931     }
   2932 
   2933     const ImVec2 title_size(title_font->CalcTextSizeA(title_font->FontSize, max_text_width, max_text_width,
   2934                                                       notif.title.c_str(), notif.title.c_str() + notif.title.size()));
   2935 
   2936     const ImVec2 text_size(text_font->CalcTextSizeA(text_font->FontSize, max_text_width, max_text_width,
   2937                                                     notif.text.c_str(), notif.text.c_str() + notif.text.size()));
   2938 
   2939     const float box_width = std::max((horizontal_padding * 2.0f) + badge_size + horizontal_spacing +
   2940                                        ImCeil(std::max(title_size.x, text_size.x)),
   2941                                      min_width);
   2942     const float box_height =
   2943       std::max((vertical_padding * 2.0f) + ImCeil(title_size.y) + vertical_spacing + ImCeil(text_size.y), min_height);
   2944 
   2945     u8 opacity;
   2946     if (time_passed < NOTIFICATION_FADE_IN_TIME)
   2947       opacity = static_cast<u8>((time_passed / NOTIFICATION_FADE_IN_TIME) * 255.0f);
   2948     else if (time_passed > (notif.duration - NOTIFICATION_FADE_OUT_TIME))
   2949       opacity = static_cast<u8>(std::min((notif.duration - time_passed) / NOTIFICATION_FADE_OUT_TIME, 1.0f) * 255.0f);
   2950     else
   2951       opacity = 255;
   2952 
   2953     const float expected_y = position.y - ((s_notification_vertical_direction < 0.0f) ? box_height : 0.0f);
   2954     float actual_y = notif.last_y;
   2955     if (notif.target_y != expected_y)
   2956     {
   2957       notif.move_time = current_time;
   2958       notif.target_y = expected_y;
   2959       notif.last_y = (notif.last_y < 0.0f) ? expected_y : notif.last_y;
   2960       actual_y = notif.last_y;
   2961     }
   2962     else if (actual_y != expected_y)
   2963     {
   2964       const float time_since_move =
   2965         static_cast<float>(Common::Timer::ConvertValueToSeconds(current_time - notif.move_time));
   2966       if (time_since_move >= MOVE_DURATION)
   2967       {
   2968         notif.move_time = current_time;
   2969         notif.last_y = notif.target_y;
   2970         actual_y = notif.last_y;
   2971       }
   2972       else
   2973       {
   2974         const float frac = Easing::OutExpo(time_since_move / MOVE_DURATION);
   2975         actual_y = notif.last_y - ((notif.last_y - notif.target_y) * frac);
   2976       }
   2977     }
   2978 
   2979     const ImVec2 box_min(position.x, actual_y);
   2980     const ImVec2 box_max(box_min.x + box_width, box_min.y + box_height);
   2981     const u32 background_color = (toast_background_color & ~IM_COL32_A_MASK) | (opacity << IM_COL32_A_SHIFT);
   2982     const u32 border_color = (toast_border_color & ~IM_COL32_A_MASK) | (opacity << IM_COL32_A_SHIFT);
   2983 
   2984     ImDrawList* dl = ImGui::GetForegroundDrawList();
   2985     dl->AddRectFilled(ImVec2(box_min.x + shadow_size, box_min.y + shadow_size),
   2986                       ImVec2(box_max.x + shadow_size, box_max.y + shadow_size),
   2987                       IM_COL32(20, 20, 20, (180 * opacity) / 255u), rounding, ImDrawFlags_RoundCornersAll);
   2988     dl->AddRectFilled(box_min, box_max, background_color, rounding, ImDrawFlags_RoundCornersAll);
   2989     dl->AddRect(box_min, box_max, border_color, rounding, ImDrawFlags_RoundCornersAll,
   2990                 ImGuiFullscreen::LayoutScale(1.0f));
   2991 
   2992     const ImVec2 badge_min(box_min.x + horizontal_padding, box_min.y + vertical_padding);
   2993     const ImVec2 badge_max(badge_min.x + badge_size, badge_min.y + badge_size);
   2994     if (!notif.badge_path.empty())
   2995     {
   2996       GPUTexture* tex = GetCachedTexture(notif.badge_path.c_str());
   2997       if (tex)
   2998       {
   2999         dl->AddImage(tex, badge_min, badge_max, ImVec2(0.0f, 0.0f), ImVec2(1.0f, 1.0f),
   3000                      IM_COL32(255, 255, 255, opacity));
   3001       }
   3002     }
   3003 
   3004     const ImVec2 title_min(badge_max.x + horizontal_spacing, box_min.y + vertical_padding);
   3005     const ImVec2 title_max(title_min.x + title_size.x, title_min.y + title_size.y);
   3006     const u32 title_col = (toast_title_color & ~IM_COL32_A_MASK) | (opacity << IM_COL32_A_SHIFT);
   3007     dl->AddText(title_font, title_font->FontSize, title_min, title_col, notif.title.c_str(),
   3008                 notif.title.c_str() + notif.title.size(), max_text_width);
   3009 
   3010     const ImVec2 text_min(badge_max.x + horizontal_spacing, title_max.y + vertical_spacing);
   3011     const ImVec2 text_max(text_min.x + text_size.x, text_min.y + text_size.y);
   3012     const u32 text_col = (toast_text_color & ~IM_COL32_A_MASK) | (opacity << IM_COL32_A_SHIFT);
   3013     dl->AddText(text_font, text_font->FontSize, text_min, text_col, notif.text.c_str(),
   3014                 notif.text.c_str() + notif.text.size(), max_text_width);
   3015 
   3016     position.y += s_notification_vertical_direction * (box_height + shadow_size + spacing);
   3017     index++;
   3018   }
   3019 }
   3020 
   3021 void ImGuiFullscreen::ShowToast(std::string title, std::string message, float duration)
   3022 {
   3023   s_toast_title = std::move(title);
   3024   s_toast_message = std::move(message);
   3025   s_toast_start_time = Common::Timer::GetCurrentValue();
   3026   s_toast_duration = duration;
   3027 }
   3028 
   3029 void ImGuiFullscreen::ClearToast()
   3030 {
   3031   s_toast_message = {};
   3032   s_toast_title = {};
   3033   s_toast_start_time = 0;
   3034   s_toast_duration = 0.0f;
   3035 }
   3036 
   3037 void ImGuiFullscreen::DrawToast()
   3038 {
   3039   if (s_toast_title.empty() && s_toast_message.empty())
   3040     return;
   3041 
   3042   const float elapsed =
   3043     static_cast<float>(Common::Timer::ConvertValueToSeconds(Common::Timer::GetCurrentValue() - s_toast_start_time));
   3044   if (elapsed >= s_toast_duration)
   3045   {
   3046     ClearToast();
   3047     return;
   3048   }
   3049 
   3050   // fade out the last second
   3051   const float alpha = std::min(std::min(elapsed * 4.0f, s_toast_duration - elapsed), 1.0f);
   3052 
   3053   const float max_width = LayoutScale(600.0f);
   3054 
   3055   ImFont* title_font = g_large_font;
   3056   ImFont* message_font = g_medium_font;
   3057   const float padding = LayoutScale(20.0f);
   3058   const float total_padding = padding * 2.0f;
   3059   const float margin = LayoutScale(20.0f + (s_fullscreen_footer_text.empty() ? 0.0f : LAYOUT_FOOTER_HEIGHT));
   3060   const float spacing = s_toast_title.empty() ? 0.0f : LayoutScale(10.0f);
   3061   const ImVec2 display_size(ImGui::GetIO().DisplaySize);
   3062   const ImVec2 title_size(s_toast_title.empty() ?
   3063                             ImVec2(0.0f, 0.0f) :
   3064                             title_font->CalcTextSizeA(title_font->FontSize, FLT_MAX, max_width, s_toast_title.c_str(),
   3065                                                       s_toast_title.c_str() + s_toast_title.length()));
   3066   const ImVec2 message_size(s_toast_message.empty() ?
   3067                               ImVec2(0.0f, 0.0f) :
   3068                               message_font->CalcTextSizeA(message_font->FontSize, FLT_MAX, max_width,
   3069                                                           s_toast_message.c_str(),
   3070                                                           s_toast_message.c_str() + s_toast_message.length()));
   3071   const ImVec2 comb_size(std::max(title_size.x, message_size.x), title_size.y + spacing + message_size.y);
   3072 
   3073   const ImVec2 box_size(comb_size.x + total_padding, comb_size.y + total_padding);
   3074   const ImVec2 box_pos((display_size.x - box_size.x) * 0.5f, (display_size.y - margin - box_size.y));
   3075 
   3076   ImDrawList* dl = ImGui::GetForegroundDrawList();
   3077   dl->AddRectFilled(box_pos, box_pos + box_size, ImGui::GetColorU32(ModAlpha(UIPrimaryColor, alpha)), padding);
   3078   if (!s_toast_title.empty())
   3079   {
   3080     const float offset = (comb_size.x - title_size.x) * 0.5f;
   3081     dl->AddText(title_font, title_font->FontSize, box_pos + ImVec2(offset + padding, padding),
   3082                 ImGui::GetColorU32(ModAlpha(UIPrimaryTextColor, alpha)), s_toast_title.c_str(),
   3083                 s_toast_title.c_str() + s_toast_title.length(), max_width);
   3084   }
   3085   if (!s_toast_message.empty())
   3086   {
   3087     const float offset = (comb_size.x - message_size.x) * 0.5f;
   3088     dl->AddText(message_font, message_font->FontSize,
   3089                 box_pos + ImVec2(offset + padding, padding + spacing + title_size.y),
   3090                 ImGui::GetColorU32(ModAlpha(UIPrimaryTextColor, alpha)), s_toast_message.c_str(),
   3091                 s_toast_message.c_str() + s_toast_message.length(), max_width);
   3092   }
   3093 }
   3094 
   3095 void ImGuiFullscreen::SetTheme(bool light)
   3096 {
   3097   s_light_theme = light;
   3098 
   3099   if (!light)
   3100   {
   3101     // dark
   3102     UIBackgroundColor = HEX_TO_IMVEC4(0x212121, 0xff);
   3103     UIBackgroundTextColor = HEX_TO_IMVEC4(0xffffff, 0xff);
   3104     UIBackgroundLineColor = HEX_TO_IMVEC4(0xf0f0f0, 0xff);
   3105     UIBackgroundHighlightColor = HEX_TO_IMVEC4(0x4b4b4b, 0xff);
   3106     UIPopupBackgroundColor = HEX_TO_IMVEC4(0x212121, 0xf2);
   3107     UIPrimaryColor = HEX_TO_IMVEC4(0x2e2e2e, 0xff);
   3108     UIPrimaryLightColor = HEX_TO_IMVEC4(0x484848, 0xff);
   3109     UIPrimaryDarkColor = HEX_TO_IMVEC4(0x000000, 0xff);
   3110     UIPrimaryTextColor = HEX_TO_IMVEC4(0xffffff, 0xff);
   3111     UIDisabledColor = HEX_TO_IMVEC4(0xaaaaaa, 0xff);
   3112     UITextHighlightColor = HEX_TO_IMVEC4(0x90caf9, 0xff);
   3113     UIPrimaryLineColor = HEX_TO_IMVEC4(0xffffff, 0xff);
   3114     UISecondaryColor = HEX_TO_IMVEC4(0x0d47a1, 0xff);
   3115     UISecondaryStrongColor = HEX_TO_IMVEC4(0x63a4ff, 0xff);
   3116     UISecondaryWeakColor = HEX_TO_IMVEC4(0x002171, 0xff);
   3117     UISecondaryTextColor = HEX_TO_IMVEC4(0xffffff, 0xff);
   3118   }
   3119   else
   3120   {
   3121     // light
   3122     UIBackgroundColor = HEX_TO_IMVEC4(0xc8c8c8, 0xff);
   3123     UIBackgroundTextColor = HEX_TO_IMVEC4(0x000000, 0xff);
   3124     UIBackgroundLineColor = HEX_TO_IMVEC4(0xe1e2e1, 0xff);
   3125     UIBackgroundHighlightColor = HEX_TO_IMVEC4(0xe1e2e1, 0xff);
   3126     UIPopupBackgroundColor = HEX_TO_IMVEC4(0xd8d8d8, 0xf2);
   3127     UIPrimaryColor = HEX_TO_IMVEC4(0x2a3e78, 0xff);
   3128     UIPrimaryLightColor = HEX_TO_IMVEC4(0x235cd9, 0xff);
   3129     UIPrimaryDarkColor = HEX_TO_IMVEC4(0x1d2953, 0xff);
   3130     UIPrimaryTextColor = HEX_TO_IMVEC4(0xffffff, 0xff);
   3131     UIDisabledColor = HEX_TO_IMVEC4(0x999999, 0xff);
   3132     UITextHighlightColor = HEX_TO_IMVEC4(0x8e8e8e, 0xff);
   3133     UIPrimaryLineColor = HEX_TO_IMVEC4(0x000000, 0xff);
   3134     UISecondaryColor = HEX_TO_IMVEC4(0x2a3e78, 0xff);
   3135     UISecondaryStrongColor = HEX_TO_IMVEC4(0x464db1, 0xff);
   3136     UISecondaryWeakColor = HEX_TO_IMVEC4(0xc0cfff, 0xff);
   3137     UISecondaryTextColor = HEX_TO_IMVEC4(0x000000, 0xff);
   3138   }
   3139 }