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 }