imgui_manager.cpp (42007B)
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_manager.h" 5 #include "gpu_device.h" 6 #include "host.h" 7 #include "image.h" 8 #include "imgui_fullscreen.h" 9 #include "imgui_glyph_ranges.inl" 10 #include "input_manager.h" 11 12 #include "common/assert.h" 13 #include "common/easing.h" 14 #include "common/error.h" 15 #include "common/file_system.h" 16 #include "common/log.h" 17 #include "common/string_util.h" 18 #include "common/timer.h" 19 20 #include "IconsFontAwesome5.h" 21 #include "fmt/format.h" 22 #include "imgui.h" 23 #include "imgui_freetype.h" 24 #include "imgui_internal.h" 25 26 #include <atomic> 27 #include <chrono> 28 #include <cmath> 29 #include <deque> 30 #include <mutex> 31 #include <type_traits> 32 #include <unordered_map> 33 34 Log_SetChannel(ImGuiManager); 35 36 namespace ImGuiManager { 37 namespace { 38 39 struct SoftwareCursor 40 { 41 std::string image_path; 42 std::unique_ptr<GPUTexture> texture; 43 u32 color; 44 float scale; 45 float extent_x; 46 float extent_y; 47 std::pair<float, float> pos; 48 }; 49 50 struct OSDMessage 51 { 52 std::string key; 53 std::string text; 54 Common::Timer::Value start_time; 55 Common::Timer::Value move_time; 56 float duration; 57 float target_y; 58 float last_y; 59 }; 60 61 } // namespace 62 63 static_assert(std::is_same_v<WCharType, ImWchar>); 64 65 static void UpdateScale(); 66 static void SetStyle(); 67 static void SetKeyMap(); 68 static bool LoadFontData(); 69 static void ReloadFontDataIfActive(); 70 static bool AddImGuiFonts(bool fullscreen_fonts); 71 static ImFont* AddTextFont(float size); 72 static ImFont* AddFixedFont(float size); 73 static bool AddIconFonts(float size); 74 static void AcquirePendingOSDMessages(Common::Timer::Value current_time); 75 static void DrawOSDMessages(Common::Timer::Value current_time); 76 static void CreateSoftwareCursorTextures(); 77 static void UpdateSoftwareCursorTexture(u32 index); 78 static void DestroySoftwareCursorTextures(); 79 static void DrawSoftwareCursor(const SoftwareCursor& sc, const std::pair<float, float>& pos); 80 81 static float s_global_prescale = 1.0f; // before window scale 82 static float s_global_scale = 1.0f; 83 84 static std::string s_font_path; 85 static std::vector<WCharType> s_font_range; 86 static std::vector<WCharType> s_emoji_range; 87 88 static ImFont* s_standard_font; 89 static ImFont* s_fixed_font; 90 static ImFont* s_medium_font; 91 static ImFont* s_large_font; 92 93 static DynamicHeapArray<u8> s_standard_font_data; 94 static DynamicHeapArray<u8> s_fixed_font_data; 95 static DynamicHeapArray<u8> s_icon_fa_font_data; 96 static DynamicHeapArray<u8> s_icon_pf_font_data; 97 static DynamicHeapArray<u8> s_emoji_font_data; 98 99 static float s_window_width; 100 static float s_window_height; 101 static Common::Timer s_last_render_time; 102 103 // cached copies of WantCaptureKeyboard/Mouse, used to know when to dispatch events 104 static std::atomic_bool s_imgui_wants_keyboard{false}; 105 static std::atomic_bool s_imgui_wants_mouse{false}; 106 107 // mapping of host key -> imgui key 108 static std::unordered_map<u32, ImGuiKey> s_imgui_key_map; 109 110 static constexpr float OSD_FADE_IN_TIME = 0.1f; 111 static constexpr float OSD_FADE_OUT_TIME = 0.4f; 112 113 static std::deque<OSDMessage> s_osd_active_messages; 114 static std::deque<OSDMessage> s_osd_posted_messages; 115 static std::mutex s_osd_messages_lock; 116 static bool s_show_osd_messages = true; 117 static bool s_scale_changed = false; 118 119 static std::array<ImGuiManager::SoftwareCursor, InputManager::MAX_SOFTWARE_CURSORS> s_software_cursors = {}; 120 } // namespace ImGuiManager 121 122 void ImGuiManager::SetFontPathAndRange(std::string path, std::vector<WCharType> range) 123 { 124 if (s_font_path == path && s_font_range == range) 125 return; 126 127 s_font_path = std::move(path); 128 s_font_range = std::move(range); 129 s_standard_font_data = {}; 130 ReloadFontDataIfActive(); 131 } 132 133 void ImGuiManager::SetEmojiFontRange(std::vector<WCharType> range) 134 { 135 static constexpr size_t builtin_size = std::size(EMOJI_ICON_RANGE); 136 const size_t runtime_size = range.size(); 137 138 if (runtime_size == 0) 139 { 140 if (s_emoji_range.empty()) 141 return; 142 143 s_emoji_range = {}; 144 } 145 else 146 { 147 if (!s_emoji_range.empty() && (s_emoji_range.size() - builtin_size) == range.size() && 148 std::memcmp(s_emoji_range.data(), range.data(), range.size() * sizeof(ImWchar)) == 0) 149 { 150 // no change 151 return; 152 } 153 154 s_emoji_range = std::move(range); 155 s_emoji_range.resize(s_emoji_range.size() + builtin_size); 156 std::memcpy(&s_emoji_range[runtime_size], EMOJI_ICON_RANGE, sizeof(EMOJI_ICON_RANGE)); 157 } 158 159 ReloadFontDataIfActive(); 160 } 161 162 std::vector<ImGuiManager::WCharType> ImGuiManager::CompactFontRange(std::span<const WCharType> range) 163 { 164 std::vector<ImWchar> ret; 165 166 for (auto it = range.begin(); it != range.end();) 167 { 168 auto next_it = it; 169 ++next_it; 170 171 // Combine sequential ranges. 172 const ImWchar start_codepoint = *it; 173 ImWchar end_codepoint = start_codepoint; 174 while (next_it != range.end()) 175 { 176 const ImWchar next_codepoint = *next_it; 177 if (next_codepoint != (end_codepoint + 1)) 178 break; 179 180 // Yep, include it. 181 end_codepoint = next_codepoint; 182 ++next_it; 183 } 184 185 ret.push_back(start_codepoint); 186 ret.push_back(end_codepoint); 187 188 it = next_it; 189 } 190 191 return ret; 192 } 193 194 void ImGuiManager::SetGlobalScale(float global_scale) 195 { 196 if (s_global_prescale == global_scale) 197 return; 198 199 s_global_prescale = global_scale; 200 s_scale_changed = true; 201 } 202 203 void ImGuiManager::SetShowOSDMessages(bool enable) 204 { 205 if (s_show_osd_messages == enable) 206 return; 207 208 s_show_osd_messages = enable; 209 if (!enable) 210 Host::ClearOSDMessages(); 211 } 212 213 bool ImGuiManager::Initialize(float global_scale, bool show_osd_messages, Error* error) 214 { 215 if (!LoadFontData()) 216 { 217 Error::SetString(error, "Failed to load font data"); 218 return false; 219 } 220 221 s_global_prescale = global_scale; 222 s_global_scale = std::max(g_gpu_device->GetWindowScale() * global_scale, 1.0f); 223 s_scale_changed = false; 224 s_show_osd_messages = show_osd_messages; 225 226 ImGui::CreateContext(); 227 228 ImGuiIO& io = ImGui::GetIO(); 229 io.IniFilename = nullptr; 230 io.BackendFlags |= ImGuiBackendFlags_HasGamepad | ImGuiBackendFlags_RendererHasVtxOffset; 231 io.BackendUsingLegacyKeyArrays = 0; 232 io.BackendUsingLegacyNavInputArray = 0; 233 io.KeyRepeatDelay = 0.5f; 234 #ifndef __ANDROID__ 235 // Android has no keyboard, nor are we using ImGui for any actual user-interactable windows. 236 io.ConfigFlags |= 237 ImGuiConfigFlags_NavEnableKeyboard | ImGuiConfigFlags_NavEnableGamepad | ImGuiConfigFlags_NoMouseCursorChange; 238 #else 239 io.ConfigFlags |= ImGuiConfigFlags_NavEnableKeyboard | ImGuiConfigFlags_NavEnableGamepad; 240 #endif 241 242 s_window_width = static_cast<float>(g_gpu_device->GetWindowWidth()); 243 s_window_height = static_cast<float>(g_gpu_device->GetWindowHeight()); 244 io.DisplayFramebufferScale = ImVec2(1, 1); // We already scale things ourselves, this would double-apply scaling 245 io.DisplaySize = ImVec2(s_window_width, s_window_height); 246 247 SetKeyMap(); 248 SetStyle(); 249 250 if (!AddImGuiFonts(false) || !g_gpu_device->UpdateImGuiFontTexture()) 251 { 252 Error::SetString(error, "Failed to create ImGui font text"); 253 ImGui::DestroyContext(); 254 return false; 255 } 256 257 // don't need the font data anymore, save some memory 258 ImGui::GetIO().Fonts->ClearTexData(); 259 260 NewFrame(); 261 262 CreateSoftwareCursorTextures(); 263 return true; 264 } 265 266 void ImGuiManager::Shutdown() 267 { 268 DestroySoftwareCursorTextures(); 269 270 if (ImGui::GetCurrentContext()) 271 ImGui::DestroyContext(); 272 273 s_standard_font = nullptr; 274 s_fixed_font = nullptr; 275 s_medium_font = nullptr; 276 s_large_font = nullptr; 277 ImGuiFullscreen::SetFonts(nullptr, nullptr, nullptr); 278 } 279 280 float ImGuiManager::GetWindowWidth() 281 { 282 return s_window_width; 283 } 284 285 float ImGuiManager::GetWindowHeight() 286 { 287 return s_window_height; 288 } 289 290 void ImGuiManager::WindowResized(float width, float height) 291 { 292 s_window_width = width; 293 s_window_height = height; 294 ImGui::GetIO().DisplaySize = ImVec2(width, height); 295 296 // Scale might have changed as a result of window resize. 297 RequestScaleUpdate(); 298 } 299 300 void ImGuiManager::RequestScaleUpdate() 301 { 302 // Might need to update the scale. 303 s_scale_changed = true; 304 } 305 306 void ImGuiManager::UpdateScale() 307 { 308 const float window_scale = g_gpu_device ? g_gpu_device->GetWindowScale() : 1.0f; 309 const float scale = std::max(window_scale * s_global_prescale, 1.0f); 310 311 if ((!HasFullscreenFonts() || !ImGuiFullscreen::UpdateLayoutScale()) && scale == s_global_scale) 312 return; 313 314 s_global_scale = scale; 315 316 ImGui::GetStyle() = ImGuiStyle(); 317 ImGui::GetStyle().WindowMinSize = ImVec2(1.0f, 1.0f); 318 SetStyle(); 319 ImGui::GetStyle().ScaleAllSizes(scale); 320 321 if (!AddImGuiFonts(HasFullscreenFonts())) 322 Panic("Failed to create ImGui font text"); 323 324 if (!g_gpu_device->UpdateImGuiFontTexture()) 325 Panic("Failed to recreate font texture after scale+resize"); 326 } 327 328 void ImGuiManager::NewFrame() 329 { 330 ImGuiIO& io = ImGui::GetIO(); 331 io.DeltaTime = static_cast<float>(s_last_render_time.GetTimeSecondsAndReset()); 332 333 if (s_scale_changed) 334 { 335 s_scale_changed = false; 336 UpdateScale(); 337 } 338 339 ImGui::NewFrame(); 340 341 // Disable nav input on the implicit (Debug##Default) window. Otherwise we end up requesting keyboard 342 // focus when there's nothing there. We use GetCurrentWindowRead() because otherwise it'll make it visible. 343 ImGui::GetCurrentWindowRead()->Flags |= ImGuiWindowFlags_NoNavInputs; 344 s_imgui_wants_keyboard.store(io.WantCaptureKeyboard, std::memory_order_relaxed); 345 s_imgui_wants_mouse.store(io.WantCaptureMouse, std::memory_order_release); 346 } 347 348 void ImGuiManager::SetStyle() 349 { 350 ImGuiStyle& style = ImGui::GetStyle(); 351 style = ImGuiStyle(); 352 style.WindowMinSize = ImVec2(1.0f, 1.0f); 353 354 ImVec4* colors = style.Colors; 355 colors[ImGuiCol_Text] = ImVec4(0.95f, 0.96f, 0.98f, 1.00f); 356 colors[ImGuiCol_TextDisabled] = ImVec4(0.36f, 0.42f, 0.47f, 1.00f); 357 colors[ImGuiCol_WindowBg] = ImVec4(0.11f, 0.15f, 0.17f, 1.00f); 358 colors[ImGuiCol_ChildBg] = ImVec4(0.15f, 0.18f, 0.22f, 1.00f); 359 colors[ImGuiCol_PopupBg] = ImVec4(0.08f, 0.08f, 0.08f, 0.94f); 360 colors[ImGuiCol_Border] = ImVec4(0.08f, 0.10f, 0.12f, 1.00f); 361 colors[ImGuiCol_BorderShadow] = ImVec4(0.00f, 0.00f, 0.00f, 0.00f); 362 colors[ImGuiCol_FrameBg] = ImVec4(0.20f, 0.25f, 0.29f, 1.00f); 363 colors[ImGuiCol_FrameBgHovered] = ImVec4(0.12f, 0.20f, 0.28f, 1.00f); 364 colors[ImGuiCol_FrameBgActive] = ImVec4(0.09f, 0.12f, 0.14f, 1.00f); 365 colors[ImGuiCol_TitleBg] = ImVec4(0.09f, 0.12f, 0.14f, 0.65f); 366 colors[ImGuiCol_TitleBgActive] = ImVec4(0.08f, 0.10f, 0.12f, 1.00f); 367 colors[ImGuiCol_TitleBgCollapsed] = ImVec4(0.00f, 0.00f, 0.00f, 0.51f); 368 colors[ImGuiCol_MenuBarBg] = ImVec4(0.15f, 0.18f, 0.22f, 1.00f); 369 colors[ImGuiCol_ScrollbarBg] = ImVec4(0.02f, 0.02f, 0.02f, 0.39f); 370 colors[ImGuiCol_ScrollbarGrab] = ImVec4(0.20f, 0.25f, 0.29f, 1.00f); 371 colors[ImGuiCol_ScrollbarGrabHovered] = ImVec4(0.18f, 0.22f, 0.25f, 1.00f); 372 colors[ImGuiCol_ScrollbarGrabActive] = ImVec4(0.09f, 0.21f, 0.31f, 1.00f); 373 colors[ImGuiCol_CheckMark] = ImVec4(0.28f, 0.56f, 1.00f, 1.00f); 374 colors[ImGuiCol_SliderGrab] = ImVec4(0.28f, 0.56f, 1.00f, 1.00f); 375 colors[ImGuiCol_SliderGrabActive] = ImVec4(0.37f, 0.61f, 1.00f, 1.00f); 376 colors[ImGuiCol_Button] = ImVec4(0.20f, 0.25f, 0.29f, 1.00f); 377 colors[ImGuiCol_ButtonHovered] = ImVec4(0.33f, 0.38f, 0.46f, 1.00f); 378 colors[ImGuiCol_ButtonActive] = ImVec4(0.27f, 0.32f, 0.38f, 1.00f); 379 colors[ImGuiCol_Header] = ImVec4(0.20f, 0.25f, 0.29f, 0.55f); 380 colors[ImGuiCol_HeaderHovered] = ImVec4(0.33f, 0.38f, 0.46f, 1.00f); 381 colors[ImGuiCol_HeaderActive] = ImVec4(0.27f, 0.32f, 0.38f, 1.00f); 382 colors[ImGuiCol_Separator] = ImVec4(0.20f, 0.25f, 0.29f, 1.00f); 383 colors[ImGuiCol_SeparatorHovered] = ImVec4(0.33f, 0.38f, 0.46f, 1.00f); 384 colors[ImGuiCol_SeparatorActive] = ImVec4(0.27f, 0.32f, 0.38f, 1.00f); 385 colors[ImGuiCol_ResizeGrip] = ImVec4(0.26f, 0.59f, 0.98f, 0.25f); 386 colors[ImGuiCol_ResizeGripHovered] = ImVec4(0.33f, 0.38f, 0.46f, 1.00f); 387 colors[ImGuiCol_ResizeGripActive] = ImVec4(0.27f, 0.32f, 0.38f, 1.00f); 388 colors[ImGuiCol_Tab] = ImVec4(0.11f, 0.15f, 0.17f, 1.00f); 389 colors[ImGuiCol_TabHovered] = ImVec4(0.33f, 0.38f, 0.46f, 1.00f); 390 colors[ImGuiCol_TabActive] = ImVec4(0.27f, 0.32f, 0.38f, 1.00f); 391 colors[ImGuiCol_TabUnfocused] = ImVec4(0.11f, 0.15f, 0.17f, 1.00f); 392 colors[ImGuiCol_TabUnfocusedActive] = ImVec4(0.11f, 0.15f, 0.17f, 1.00f); 393 colors[ImGuiCol_PlotLines] = ImVec4(0.61f, 0.61f, 0.61f, 1.00f); 394 colors[ImGuiCol_PlotLinesHovered] = ImVec4(1.00f, 0.43f, 0.35f, 1.00f); 395 colors[ImGuiCol_PlotHistogram] = ImVec4(0.90f, 0.70f, 0.00f, 1.00f); 396 colors[ImGuiCol_PlotHistogramHovered] = ImVec4(1.00f, 0.60f, 0.00f, 1.00f); 397 colors[ImGuiCol_TextSelectedBg] = ImVec4(0.26f, 0.59f, 0.98f, 0.35f); 398 colors[ImGuiCol_DragDropTarget] = ImVec4(1.00f, 1.00f, 0.00f, 0.90f); 399 colors[ImGuiCol_NavHighlight] = ImVec4(0.26f, 0.59f, 0.98f, 1.00f); 400 colors[ImGuiCol_NavWindowingHighlight] = ImVec4(1.00f, 1.00f, 1.00f, 0.70f); 401 colors[ImGuiCol_NavWindowingDimBg] = ImVec4(0.80f, 0.80f, 0.80f, 0.20f); 402 colors[ImGuiCol_ModalWindowDimBg] = ImVec4(0.80f, 0.80f, 0.80f, 0.35f); 403 404 style.ScaleAllSizes(s_global_scale); 405 } 406 407 void ImGuiManager::SetKeyMap() 408 { 409 struct KeyMapping 410 { 411 ImGuiKey index; 412 const char* name; 413 const char* alt_name; 414 }; 415 416 static constexpr KeyMapping mapping[] = {{ImGuiKey_LeftArrow, "Left", nullptr}, 417 {ImGuiKey_RightArrow, "Right", nullptr}, 418 {ImGuiKey_UpArrow, "Up", nullptr}, 419 {ImGuiKey_DownArrow, "Down", nullptr}, 420 {ImGuiKey_PageUp, "PageUp", nullptr}, 421 {ImGuiKey_PageDown, "PageDown", nullptr}, 422 {ImGuiKey_Home, "Home", nullptr}, 423 {ImGuiKey_End, "End", nullptr}, 424 {ImGuiKey_Insert, "Insert", nullptr}, 425 {ImGuiKey_Delete, "Delete", nullptr}, 426 {ImGuiKey_Backspace, "Backspace", nullptr}, 427 {ImGuiKey_Space, "Space", nullptr}, 428 {ImGuiKey_Enter, "Return", nullptr}, 429 {ImGuiKey_Escape, "Escape", nullptr}, 430 {ImGuiKey_LeftCtrl, "LeftCtrl", "Ctrl"}, 431 {ImGuiKey_LeftShift, "LeftShift", "Shift"}, 432 {ImGuiKey_LeftAlt, "LeftAlt", "Alt"}, 433 {ImGuiKey_LeftSuper, "LeftSuper", "Super"}, 434 {ImGuiKey_RightCtrl, "RightCtrl", nullptr}, 435 {ImGuiKey_RightShift, "RightShift", nullptr}, 436 {ImGuiKey_RightAlt, "RightAlt", nullptr}, 437 {ImGuiKey_RightSuper, "RightSuper", nullptr}, 438 {ImGuiKey_Menu, "Menu", nullptr}, 439 {ImGuiKey_0, "0", nullptr}, 440 {ImGuiKey_1, "1", nullptr}, 441 {ImGuiKey_2, "2", nullptr}, 442 {ImGuiKey_3, "3", nullptr}, 443 {ImGuiKey_4, "4", nullptr}, 444 {ImGuiKey_5, "5", nullptr}, 445 {ImGuiKey_6, "6", nullptr}, 446 {ImGuiKey_7, "7", nullptr}, 447 {ImGuiKey_8, "8", nullptr}, 448 {ImGuiKey_9, "9", nullptr}, 449 {ImGuiKey_A, "A", nullptr}, 450 {ImGuiKey_B, "B", nullptr}, 451 {ImGuiKey_C, "C", nullptr}, 452 {ImGuiKey_D, "D", nullptr}, 453 {ImGuiKey_E, "E", nullptr}, 454 {ImGuiKey_F, "F", nullptr}, 455 {ImGuiKey_G, "G", nullptr}, 456 {ImGuiKey_H, "H", nullptr}, 457 {ImGuiKey_I, "I", nullptr}, 458 {ImGuiKey_J, "J", nullptr}, 459 {ImGuiKey_K, "K", nullptr}, 460 {ImGuiKey_L, "L", nullptr}, 461 {ImGuiKey_M, "M", nullptr}, 462 {ImGuiKey_N, "N", nullptr}, 463 {ImGuiKey_O, "O", nullptr}, 464 {ImGuiKey_P, "P", nullptr}, 465 {ImGuiKey_Q, "Q", nullptr}, 466 {ImGuiKey_R, "R", nullptr}, 467 {ImGuiKey_S, "S", nullptr}, 468 {ImGuiKey_T, "T", nullptr}, 469 {ImGuiKey_U, "U", nullptr}, 470 {ImGuiKey_V, "V", nullptr}, 471 {ImGuiKey_W, "W", nullptr}, 472 {ImGuiKey_X, "X", nullptr}, 473 {ImGuiKey_Y, "Y", nullptr}, 474 {ImGuiKey_Z, "Z", nullptr}, 475 {ImGuiKey_F1, "F1", nullptr}, 476 {ImGuiKey_F2, "F2", nullptr}, 477 {ImGuiKey_F3, "F3", nullptr}, 478 {ImGuiKey_F4, "F4", nullptr}, 479 {ImGuiKey_F5, "F5", nullptr}, 480 {ImGuiKey_F6, "F6", nullptr}, 481 {ImGuiKey_F7, "F7", nullptr}, 482 {ImGuiKey_F8, "F8", nullptr}, 483 {ImGuiKey_F9, "F9", nullptr}, 484 {ImGuiKey_F10, "F10", nullptr}, 485 {ImGuiKey_F11, "F11", nullptr}, 486 {ImGuiKey_F12, "F12", nullptr}, 487 {ImGuiKey_Apostrophe, "Apostrophe", nullptr}, 488 {ImGuiKey_Comma, "Comma", nullptr}, 489 {ImGuiKey_Minus, "Minus", nullptr}, 490 {ImGuiKey_Period, "Period", nullptr}, 491 {ImGuiKey_Slash, "Slash", nullptr}, 492 {ImGuiKey_Semicolon, "Semicolon", nullptr}, 493 {ImGuiKey_Equal, "Equal", nullptr}, 494 {ImGuiKey_LeftBracket, "BracketLeft", nullptr}, 495 {ImGuiKey_Backslash, "Backslash", nullptr}, 496 {ImGuiKey_RightBracket, "BracketRight", nullptr}, 497 {ImGuiKey_GraveAccent, "QuoteLeft", nullptr}, 498 {ImGuiKey_CapsLock, "CapsLock", nullptr}, 499 {ImGuiKey_ScrollLock, "ScrollLock", nullptr}, 500 {ImGuiKey_NumLock, "NumLock", nullptr}, 501 {ImGuiKey_PrintScreen, "PrintScreen", nullptr}, 502 {ImGuiKey_Pause, "Pause", nullptr}, 503 {ImGuiKey_Keypad0, "Keypad0", nullptr}, 504 {ImGuiKey_Keypad1, "Keypad1", nullptr}, 505 {ImGuiKey_Keypad2, "Keypad2", nullptr}, 506 {ImGuiKey_Keypad3, "Keypad3", nullptr}, 507 {ImGuiKey_Keypad4, "Keypad4", nullptr}, 508 {ImGuiKey_Keypad5, "Keypad5", nullptr}, 509 {ImGuiKey_Keypad6, "Keypad6", nullptr}, 510 {ImGuiKey_Keypad7, "Keypad7", nullptr}, 511 {ImGuiKey_Keypad8, "Keypad8", nullptr}, 512 {ImGuiKey_Keypad9, "Keypad9", nullptr}, 513 {ImGuiKey_KeypadDecimal, "KeypadPeriod", nullptr}, 514 {ImGuiKey_KeypadDivide, "KeypadDivide", nullptr}, 515 {ImGuiKey_KeypadMultiply, "KeypadMultiply", nullptr}, 516 {ImGuiKey_KeypadSubtract, "KeypadMinus", nullptr}, 517 {ImGuiKey_KeypadAdd, "KeypadPlus", nullptr}, 518 {ImGuiKey_KeypadEnter, "KeypadReturn", nullptr}, 519 {ImGuiKey_KeypadEqual, "KeypadEqual", nullptr}}; 520 521 s_imgui_key_map.clear(); 522 for (const KeyMapping& km : mapping) 523 { 524 std::optional<u32> map(InputManager::ConvertHostKeyboardStringToCode(km.name)); 525 if (!map.has_value() && km.alt_name) 526 map = InputManager::ConvertHostKeyboardStringToCode(km.alt_name); 527 if (map.has_value()) 528 s_imgui_key_map[map.value()] = km.index; 529 } 530 } 531 532 bool ImGuiManager::LoadFontData() 533 { 534 if (s_standard_font_data.empty()) 535 { 536 std::optional<DynamicHeapArray<u8>> font_data = s_font_path.empty() ? 537 Host::ReadResourceFile("fonts/Roboto-Regular.ttf", true) : 538 FileSystem::ReadBinaryFile(s_font_path.c_str()); 539 if (!font_data.has_value()) 540 return false; 541 542 s_standard_font_data = std::move(font_data.value()); 543 } 544 545 if (s_fixed_font_data.empty()) 546 { 547 std::optional<DynamicHeapArray<u8>> font_data = Host::ReadResourceFile("fonts/RobotoMono-Medium.ttf", true); 548 if (!font_data.has_value()) 549 return false; 550 551 s_fixed_font_data = std::move(font_data.value()); 552 } 553 554 if (s_icon_fa_font_data.empty()) 555 { 556 std::optional<DynamicHeapArray<u8>> font_data = Host::ReadResourceFile("fonts/fa-solid-900.ttf", true); 557 if (!font_data.has_value()) 558 return false; 559 560 s_icon_fa_font_data = std::move(font_data.value()); 561 } 562 563 if (s_icon_pf_font_data.empty()) 564 { 565 std::optional<DynamicHeapArray<u8>> font_data = Host::ReadResourceFile("fonts/promptfont.otf", true); 566 if (!font_data.has_value()) 567 return false; 568 569 s_icon_pf_font_data = std::move(font_data.value()); 570 } 571 572 if (s_emoji_font_data.empty()) 573 { 574 std::optional<DynamicHeapArray<u8>> font_data = 575 Host::ReadCompressedResourceFile("fonts/TwitterColorEmoji-SVGinOT.ttf.zst", true); 576 if (!font_data.has_value()) 577 return false; 578 579 s_emoji_font_data = std::move(font_data.value()); 580 } 581 582 return true; 583 } 584 585 ImFont* ImGuiManager::AddTextFont(float size) 586 { 587 ImFontConfig cfg; 588 cfg.FontDataOwnedByAtlas = false; 589 return ImGui::GetIO().Fonts->AddFontFromMemoryTTF( 590 s_standard_font_data.data(), static_cast<int>(s_standard_font_data.size()), size, &cfg, s_font_range.data()); 591 } 592 593 ImFont* ImGuiManager::AddFixedFont(float size) 594 { 595 ImFontConfig cfg; 596 cfg.FontDataOwnedByAtlas = false; 597 return ImGui::GetIO().Fonts->AddFontFromMemoryTTF(s_fixed_font_data.data(), 598 static_cast<int>(s_fixed_font_data.size()), size, &cfg, nullptr); 599 } 600 601 bool ImGuiManager::AddIconFonts(float size) 602 { 603 { 604 ImFontConfig cfg; 605 cfg.MergeMode = true; 606 cfg.PixelSnapH = true; 607 cfg.GlyphMinAdvanceX = size; 608 cfg.GlyphMaxAdvanceX = size; 609 cfg.FontDataOwnedByAtlas = false; 610 611 if (!ImGui::GetIO().Fonts->AddFontFromMemoryTTF( 612 s_icon_fa_font_data.data(), static_cast<int>(s_icon_fa_font_data.size()), size * 0.75f, &cfg, FA_ICON_RANGE)) 613 [[unlikely]] 614 { 615 return false; 616 } 617 } 618 619 { 620 ImFontConfig cfg; 621 cfg.MergeMode = true; 622 cfg.PixelSnapH = true; 623 cfg.GlyphMinAdvanceX = size; 624 cfg.GlyphMaxAdvanceX = size; 625 cfg.FontDataOwnedByAtlas = false; 626 627 if (!ImGui::GetIO().Fonts->AddFontFromMemoryTTF( 628 s_icon_pf_font_data.data(), static_cast<int>(s_icon_pf_font_data.size()), size * 1.2f, &cfg, PF_ICON_RANGE)) 629 [[unlikely]] 630 { 631 return false; 632 } 633 } 634 635 { 636 ImFontConfig cfg; 637 cfg.MergeMode = true; 638 cfg.PixelSnapH = true; 639 cfg.GlyphMinAdvanceX = size; 640 cfg.GlyphMaxAdvanceX = size; 641 cfg.FontDataOwnedByAtlas = false; 642 cfg.FontBuilderFlags = ImGuiFreeTypeBuilderFlags_LoadColor | ImGuiFreeTypeBuilderFlags_Bitmap; 643 644 if (!ImGui::GetIO().Fonts->AddFontFromMemoryTTF( 645 s_emoji_font_data.data(), static_cast<int>(s_emoji_font_data.size()), size * 0.9f, &cfg, 646 s_emoji_range.empty() ? EMOJI_ICON_RANGE : s_emoji_range.data())) [[unlikely]] 647 { 648 return false; 649 } 650 } 651 652 return true; 653 } 654 655 bool ImGuiManager::AddImGuiFonts(bool fullscreen_fonts) 656 { 657 const float standard_font_size = std::ceil(18.0f * s_global_scale); 658 const float fixed_font_size = std::ceil(15.0f * s_global_scale); 659 660 ImGuiIO& io = ImGui::GetIO(); 661 io.Fonts->Clear(); 662 663 s_standard_font = AddTextFont(standard_font_size); 664 if (!s_standard_font || !AddIconFonts(standard_font_size)) 665 return false; 666 667 s_fixed_font = AddFixedFont(fixed_font_size); 668 if (!s_fixed_font) 669 return false; 670 671 if (fullscreen_fonts) 672 { 673 const float medium_font_size = ImGuiFullscreen::LayoutScale(ImGuiFullscreen::LAYOUT_MEDIUM_FONT_SIZE); 674 s_medium_font = AddTextFont(medium_font_size); 675 if (!s_medium_font || !AddIconFonts(medium_font_size)) 676 return false; 677 678 const float large_font_size = ImGuiFullscreen::LayoutScale(ImGuiFullscreen::LAYOUT_LARGE_FONT_SIZE); 679 s_large_font = AddTextFont(large_font_size); 680 if (!s_large_font || !AddIconFonts(large_font_size)) 681 return false; 682 } 683 else 684 { 685 s_medium_font = nullptr; 686 s_large_font = nullptr; 687 } 688 689 ImGuiFullscreen::SetFonts(s_standard_font, s_medium_font, s_large_font); 690 691 return io.Fonts->Build(); 692 } 693 694 void ImGuiManager::ReloadFontDataIfActive() 695 { 696 if (!ImGui::GetCurrentContext()) 697 return; 698 699 ImGui::EndFrame(); 700 701 if (!LoadFontData()) 702 Panic("Failed to load font data"); 703 704 if (!AddImGuiFonts(HasFullscreenFonts())) 705 Panic("Failed to create ImGui font text"); 706 707 if (!g_gpu_device->UpdateImGuiFontTexture()) 708 Panic("Failed to recreate font texture after scale+resize"); 709 710 NewFrame(); 711 } 712 713 bool ImGuiManager::AddFullscreenFontsIfMissing() 714 { 715 if (HasFullscreenFonts()) 716 return true; 717 718 // can't do this in the middle of a frame 719 ImGui::EndFrame(); 720 721 if (!AddImGuiFonts(true)) 722 { 723 ERROR_LOG("Failed to lazily allocate fullscreen fonts."); 724 AddImGuiFonts(false); 725 } 726 727 g_gpu_device->UpdateImGuiFontTexture(); 728 NewFrame(); 729 730 return HasFullscreenFonts(); 731 } 732 733 bool ImGuiManager::HasFullscreenFonts() 734 { 735 return (s_medium_font && s_large_font); 736 } 737 738 void Host::AddOSDMessage(std::string message, float duration /*= 2.0f*/) 739 { 740 AddKeyedOSDMessage(std::string(), std::move(message), duration); 741 } 742 743 void Host::AddKeyedOSDMessage(std::string key, std::string message, float duration /* = 2.0f */) 744 { 745 if (!key.empty()) 746 INFO_LOG("OSD [{}]: {}", key, message); 747 else 748 INFO_LOG("OSD: {}", message); 749 750 if (!ImGuiManager::s_show_osd_messages) 751 return; 752 753 const Common::Timer::Value current_time = Common::Timer::GetCurrentValue(); 754 755 ImGuiManager::OSDMessage msg; 756 msg.key = std::move(key); 757 msg.text = std::move(message); 758 msg.duration = duration; 759 msg.start_time = current_time; 760 msg.move_time = current_time; 761 msg.target_y = -1.0f; 762 msg.last_y = -1.0f; 763 764 std::unique_lock<std::mutex> lock(ImGuiManager::s_osd_messages_lock); 765 ImGuiManager::s_osd_posted_messages.push_back(std::move(msg)); 766 } 767 768 void Host::AddIconOSDMessage(std::string key, const char* icon, std::string message, float duration /* = 2.0f */) 769 { 770 return AddKeyedOSDMessage(std::move(key), fmt::format("{} {}", icon, message), duration); 771 } 772 773 void Host::RemoveKeyedOSDMessage(std::string key) 774 { 775 if (!ImGuiManager::s_show_osd_messages) 776 return; 777 778 ImGuiManager::OSDMessage msg = {}; 779 msg.key = std::move(key); 780 msg.duration = 0.0f; 781 782 std::unique_lock<std::mutex> lock(ImGuiManager::s_osd_messages_lock); 783 ImGuiManager::s_osd_posted_messages.push_back(std::move(msg)); 784 } 785 786 void Host::ClearOSDMessages() 787 { 788 { 789 std::unique_lock<std::mutex> lock(ImGuiManager::s_osd_messages_lock); 790 ImGuiManager::s_osd_posted_messages.clear(); 791 } 792 793 ImGuiManager::s_osd_active_messages.clear(); 794 } 795 796 void ImGuiManager::AcquirePendingOSDMessages(Common::Timer::Value current_time) 797 { 798 std::atomic_thread_fence(std::memory_order_consume); 799 if (s_osd_posted_messages.empty()) 800 return; 801 802 std::unique_lock lock(s_osd_messages_lock); 803 for (;;) 804 { 805 if (s_osd_posted_messages.empty()) 806 break; 807 808 OSDMessage& new_msg = s_osd_posted_messages.front(); 809 std::deque<OSDMessage>::iterator iter; 810 if (!new_msg.key.empty() && (iter = std::find_if(s_osd_active_messages.begin(), s_osd_active_messages.end(), 811 [&new_msg](const OSDMessage& other) { 812 return new_msg.key == other.key; 813 })) != s_osd_active_messages.end()) 814 { 815 iter->text = std::move(new_msg.text); 816 iter->duration = new_msg.duration; 817 818 // Don't fade it in again 819 const float time_passed = 820 static_cast<float>(Common::Timer::ConvertValueToSeconds(current_time - iter->start_time)); 821 iter->start_time = current_time - Common::Timer::ConvertSecondsToValue(std::min(time_passed, OSD_FADE_IN_TIME)); 822 } 823 else 824 { 825 s_osd_active_messages.push_back(std::move(new_msg)); 826 } 827 828 s_osd_posted_messages.pop_front(); 829 830 static constexpr size_t MAX_ACTIVE_OSD_MESSAGES = 512; 831 if (s_osd_active_messages.size() > MAX_ACTIVE_OSD_MESSAGES) 832 s_osd_active_messages.pop_front(); 833 } 834 } 835 836 void ImGuiManager::DrawOSDMessages(Common::Timer::Value current_time) 837 { 838 static constexpr float MOVE_DURATION = 0.5f; 839 840 ImFont* const font = ImGui::GetFont(); 841 const float scale = s_global_scale; 842 const float spacing = std::ceil(6.0f * scale); 843 const float margin = std::ceil(12.0f * scale); 844 const float padding = std::ceil(10.0f * scale); 845 const float rounding = std::ceil(6.0f * scale); 846 const float max_width = s_window_width - (margin + padding) * 2.0f; 847 float position_x = margin; 848 float position_y = margin; 849 850 auto iter = s_osd_active_messages.begin(); 851 while (iter != s_osd_active_messages.end()) 852 { 853 OSDMessage& msg = *iter; 854 const float time_passed = static_cast<float>(Common::Timer::ConvertValueToSeconds(current_time - msg.start_time)); 855 if (time_passed >= msg.duration) 856 { 857 iter = s_osd_active_messages.erase(iter); 858 continue; 859 } 860 861 ++iter; 862 863 u8 opacity; 864 if (time_passed < OSD_FADE_IN_TIME) 865 opacity = static_cast<u8>((time_passed / OSD_FADE_IN_TIME) * 255.0f); 866 else if (time_passed > (msg.duration - OSD_FADE_OUT_TIME)) 867 opacity = static_cast<u8>(std::min((msg.duration - time_passed) / OSD_FADE_OUT_TIME, 1.0f) * 255.0f); 868 else 869 opacity = 255; 870 871 const float expected_y = position_y; 872 float actual_y = msg.last_y; 873 if (msg.target_y != expected_y) 874 { 875 if (msg.last_y < 0.0f) 876 { 877 // First showing. 878 msg.last_y = expected_y; 879 } 880 else 881 { 882 // We got repositioned, probably due to another message above getting removed. 883 const float time_since_move = 884 static_cast<float>(Common::Timer::ConvertValueToSeconds(current_time - msg.move_time)); 885 const float frac = Easing::OutExpo(time_since_move / MOVE_DURATION); 886 msg.last_y = std::floor(msg.last_y - ((msg.last_y - msg.target_y) * frac)); 887 } 888 889 msg.move_time = current_time; 890 msg.target_y = expected_y; 891 actual_y = msg.last_y; 892 } 893 else if (actual_y != expected_y) 894 { 895 const float time_since_move = 896 static_cast<float>(Common::Timer::ConvertValueToSeconds(current_time - msg.move_time)); 897 if (time_since_move >= MOVE_DURATION) 898 { 899 msg.move_time = current_time; 900 msg.last_y = msg.target_y; 901 actual_y = msg.last_y; 902 } 903 else 904 { 905 const float frac = Easing::OutExpo(time_since_move / MOVE_DURATION); 906 actual_y = std::floor(msg.last_y - ((msg.last_y - msg.target_y) * frac)); 907 } 908 } 909 910 if (actual_y >= ImGui::GetIO().DisplaySize.y) 911 break; 912 913 const ImVec2 pos(position_x, actual_y); 914 const ImVec2 text_size(font->CalcTextSizeA(font->FontSize, max_width, max_width, msg.text.c_str(), 915 msg.text.c_str() + msg.text.length())); 916 const ImVec2 size(text_size.x + padding * 2.0f, text_size.y + padding * 2.0f); 917 const ImVec4 text_rect(pos.x + padding, pos.y + padding, pos.x + size.x - padding, pos.y + size.y - padding); 918 919 ImDrawList* dl = ImGui::GetForegroundDrawList(); 920 dl->AddRectFilled(pos, ImVec2(pos.x + size.x, pos.y + size.y), IM_COL32(0x21, 0x21, 0x21, opacity), rounding); 921 dl->AddRect(pos, ImVec2(pos.x + size.x, pos.y + size.y), IM_COL32(0x48, 0x48, 0x48, opacity), rounding); 922 dl->AddText(font, font->FontSize, ImVec2(text_rect.x, text_rect.y), IM_COL32(0xff, 0xff, 0xff, opacity), 923 msg.text.c_str(), msg.text.c_str() + msg.text.length(), max_width, &text_rect); 924 position_y += size.y + spacing; 925 } 926 } 927 928 void ImGuiManager::RenderOSDMessages() 929 { 930 const Common::Timer::Value current_time = Common::Timer::GetCurrentValue(); 931 AcquirePendingOSDMessages(current_time); 932 DrawOSDMessages(current_time); 933 } 934 935 float ImGuiManager::GetGlobalScale() 936 { 937 return s_global_scale; 938 } 939 940 ImFont* ImGuiManager::GetStandardFont() 941 { 942 return s_standard_font; 943 } 944 945 ImFont* ImGuiManager::GetFixedFont() 946 { 947 return s_fixed_font; 948 } 949 950 ImFont* ImGuiManager::GetMediumFont() 951 { 952 AddFullscreenFontsIfMissing(); 953 return s_medium_font; 954 } 955 956 ImFont* ImGuiManager::GetLargeFont() 957 { 958 AddFullscreenFontsIfMissing(); 959 return s_large_font; 960 } 961 962 bool ImGuiManager::WantsTextInput() 963 { 964 return s_imgui_wants_keyboard.load(std::memory_order_acquire); 965 } 966 967 bool ImGuiManager::WantsMouseInput() 968 { 969 return s_imgui_wants_mouse.load(std::memory_order_acquire); 970 } 971 972 void ImGuiManager::AddTextInput(std::string str) 973 { 974 if (!ImGui::GetCurrentContext()) 975 return; 976 977 if (!s_imgui_wants_keyboard.load(std::memory_order_acquire)) 978 return; 979 980 ImGui::GetIO().AddInputCharactersUTF8(str.c_str()); 981 } 982 983 void ImGuiManager::UpdateMousePosition(float x, float y) 984 { 985 if (!ImGui::GetCurrentContext()) 986 return; 987 988 ImGui::GetIO().MousePos = ImVec2(x, y); 989 std::atomic_thread_fence(std::memory_order_release); 990 } 991 992 bool ImGuiManager::ProcessPointerButtonEvent(InputBindingKey key, float value) 993 { 994 if (!ImGui::GetCurrentContext() || key.data >= std::size(ImGui::GetIO().MouseDown)) 995 return false; 996 997 // still update state anyway 998 ImGui::GetIO().AddMouseButtonEvent(key.data, value != 0.0f); 999 1000 return s_imgui_wants_mouse.load(std::memory_order_acquire); 1001 } 1002 1003 bool ImGuiManager::ProcessPointerAxisEvent(InputBindingKey key, float value) 1004 { 1005 if (!ImGui::GetCurrentContext() || key.data < static_cast<u32>(InputPointerAxis::WheelX)) 1006 return false; 1007 1008 // still update state anyway 1009 const bool horizontal = (key.data == static_cast<u32>(InputPointerAxis::WheelX)); 1010 ImGui::GetIO().AddMouseWheelEvent(horizontal ? value : 0.0f, horizontal ? 0.0f : value); 1011 1012 return s_imgui_wants_mouse.load(std::memory_order_acquire); 1013 } 1014 1015 bool ImGuiManager::ProcessHostKeyEvent(InputBindingKey key, float value) 1016 { 1017 decltype(s_imgui_key_map)::iterator iter; 1018 if (!ImGui::GetCurrentContext() || (iter = s_imgui_key_map.find(key.data)) == s_imgui_key_map.end()) 1019 return false; 1020 1021 // still update state anyway 1022 ImGui::GetIO().AddKeyEvent(iter->second, value != 0.0); 1023 1024 return s_imgui_wants_keyboard.load(std::memory_order_acquire); 1025 } 1026 1027 bool ImGuiManager::ProcessGenericInputEvent(GenericInputBinding key, float value) 1028 { 1029 static constexpr ImGuiKey key_map[] = { 1030 ImGuiKey_None, // Unknown, 1031 ImGuiKey_GamepadDpadUp, // DPadUp 1032 ImGuiKey_GamepadDpadRight, // DPadRight 1033 ImGuiKey_GamepadDpadLeft, // DPadLeft 1034 ImGuiKey_GamepadDpadDown, // DPadDown 1035 ImGuiKey_None, // LeftStickUp 1036 ImGuiKey_None, // LeftStickRight 1037 ImGuiKey_None, // LeftStickDown 1038 ImGuiKey_None, // LeftStickLeft 1039 ImGuiKey_GamepadL3, // L3 1040 ImGuiKey_None, // RightStickUp 1041 ImGuiKey_None, // RightStickRight 1042 ImGuiKey_None, // RightStickDown 1043 ImGuiKey_None, // RightStickLeft 1044 ImGuiKey_GamepadR3, // R3 1045 ImGuiKey_GamepadFaceUp, // Triangle 1046 ImGuiKey_GamepadFaceRight, // Circle 1047 ImGuiKey_GamepadFaceDown, // Cross 1048 ImGuiKey_GamepadFaceLeft, // Square 1049 ImGuiKey_GamepadBack, // Select 1050 ImGuiKey_GamepadStart, // Start 1051 ImGuiKey_None, // System 1052 ImGuiKey_GamepadL1, // L1 1053 ImGuiKey_GamepadL2, // L2 1054 ImGuiKey_GamepadR1, // R1 1055 ImGuiKey_GamepadL2, // R2 1056 }; 1057 1058 if (!ImGui::GetCurrentContext()) 1059 return false; 1060 1061 if (static_cast<u32>(key) >= std::size(key_map) || key_map[static_cast<u32>(key)] == ImGuiKey_None) 1062 return false; 1063 1064 ImGui::GetIO().AddKeyAnalogEvent(key_map[static_cast<u32>(key)], (value > 0.0f), value); 1065 return s_imgui_wants_keyboard.load(std::memory_order_acquire); 1066 } 1067 1068 void ImGuiManager::CreateSoftwareCursorTextures() 1069 { 1070 for (u32 i = 0; i < static_cast<u32>(s_software_cursors.size()); i++) 1071 { 1072 if (!s_software_cursors[i].image_path.empty()) 1073 UpdateSoftwareCursorTexture(i); 1074 } 1075 } 1076 1077 void ImGuiManager::DestroySoftwareCursorTextures() 1078 { 1079 for (SoftwareCursor& sc : s_software_cursors) 1080 sc.texture.reset(); 1081 } 1082 1083 void ImGuiManager::UpdateSoftwareCursorTexture(u32 index) 1084 { 1085 SoftwareCursor& sc = s_software_cursors[index]; 1086 if (sc.image_path.empty()) 1087 { 1088 sc.texture.reset(); 1089 return; 1090 } 1091 1092 RGBA8Image image; 1093 if (!image.LoadFromFile(sc.image_path.c_str())) 1094 { 1095 ERROR_LOG("Failed to load software cursor {} image '{}'", index, sc.image_path); 1096 return; 1097 } 1098 g_gpu_device->RecycleTexture(std::move(sc.texture)); 1099 sc.texture = g_gpu_device->FetchTexture(image.GetWidth(), image.GetHeight(), 1, 1, 1, GPUTexture::Type::Texture, 1100 GPUTexture::Format::RGBA8, image.GetPixels(), image.GetPitch()); 1101 if (!sc.texture) 1102 { 1103 ERROR_LOG("Failed to upload {}x{} software cursor {} image '{}'", image.GetWidth(), image.GetHeight(), index, 1104 sc.image_path); 1105 return; 1106 } 1107 1108 sc.extent_x = std::ceil(static_cast<float>(image.GetWidth()) * sc.scale * s_global_scale) / 2.0f; 1109 sc.extent_y = std::ceil(static_cast<float>(image.GetHeight()) * sc.scale * s_global_scale) / 2.0f; 1110 } 1111 1112 void ImGuiManager::DrawSoftwareCursor(const SoftwareCursor& sc, const std::pair<float, float>& pos) 1113 { 1114 if (!sc.texture) 1115 return; 1116 1117 const ImVec2 min(pos.first - sc.extent_x, pos.second - sc.extent_y); 1118 const ImVec2 max(pos.first + sc.extent_x, pos.second + sc.extent_y); 1119 1120 ImDrawList* dl = ImGui::GetForegroundDrawList(); 1121 1122 dl->AddImage(reinterpret_cast<ImTextureID>(sc.texture.get()), min, max, ImVec2(0.0f, 0.0f), ImVec2(1.0f, 1.0f), 1123 sc.color); 1124 } 1125 1126 void ImGuiManager::RenderSoftwareCursors() 1127 { 1128 // This one's okay to race, worst that happens is we render the wrong number of cursors for a frame. 1129 const u32 pointer_count = InputManager::GetPointerCount(); 1130 for (u32 i = 0; i < pointer_count; i++) 1131 DrawSoftwareCursor(s_software_cursors[i], InputManager::GetPointerAbsolutePosition(i)); 1132 1133 for (u32 i = InputManager::MAX_POINTER_DEVICES; i < InputManager::MAX_SOFTWARE_CURSORS; i++) 1134 DrawSoftwareCursor(s_software_cursors[i], s_software_cursors[i].pos); 1135 } 1136 1137 void ImGuiManager::SetSoftwareCursor(u32 index, std::string image_path, float image_scale, u32 multiply_color) 1138 { 1139 DebugAssert(index < std::size(s_software_cursors)); 1140 SoftwareCursor& sc = s_software_cursors[index]; 1141 sc.color = multiply_color | 0xFF000000; 1142 if (sc.image_path == image_path && sc.scale == image_scale) 1143 return; 1144 1145 const bool is_hiding_or_showing = (image_path.empty() != sc.image_path.empty()); 1146 sc.image_path = std::move(image_path); 1147 sc.scale = image_scale; 1148 if (g_gpu_device) 1149 UpdateSoftwareCursorTexture(index); 1150 1151 // Hide the system cursor when we activate a software cursor. 1152 if (is_hiding_or_showing && index <= InputManager::MAX_POINTER_DEVICES) 1153 InputManager::UpdateRelativeMouseMode(); 1154 } 1155 1156 bool ImGuiManager::HasSoftwareCursor(u32 index) 1157 { 1158 return (index < s_software_cursors.size() && !s_software_cursors[index].image_path.empty()); 1159 } 1160 1161 void ImGuiManager::ClearSoftwareCursor(u32 index) 1162 { 1163 SetSoftwareCursor(index, std::string(), 0.0f, 0); 1164 } 1165 1166 void ImGuiManager::SetSoftwareCursorPosition(u32 index, float pos_x, float pos_y) 1167 { 1168 DebugAssert(index >= InputManager::MAX_POINTER_DEVICES); 1169 SoftwareCursor& sc = s_software_cursors[index]; 1170 sc.pos.first = pos_x; 1171 sc.pos.second = pos_y; 1172 } 1173 1174 std::string ImGuiManager::StripIconCharacters(std::string_view str) 1175 { 1176 std::string result; 1177 result.reserve(str.length()); 1178 1179 for (size_t offset = 0; offset < str.length();) 1180 { 1181 char32_t utf; 1182 offset += StringUtil::DecodeUTF8(str, offset, &utf); 1183 1184 // icon if outside BMP/SMP/TIP, or inside private use area 1185 if (utf > 0x32FFF || (utf >= 0xE000 && utf <= 0xF8FF)) 1186 continue; 1187 1188 StringUtil::EncodeAndAppendUTF8(result, utf); 1189 } 1190 1191 StringUtil::StripWhitespace(&result); 1192 1193 return result; 1194 }