From e422afdec14787cb9fe62b52c6863c2933056107 Mon Sep 17 00:00:00 2001 From: Stenzek Date: Fri, 14 Mar 2025 20:50:18 +1000 Subject: [PATCH] FullscreenUI: Improve achievements pause menu overlays - Add most recent unlock/nearest completion. - Make it look nicer and better fit with the rest of the interface. --- dep/imgui/include/IconsEmoji.h | 1 + src/core/achievements.cpp | 317 ++++++++++++++++++++++++-------- src/core/achievements.h | 6 +- src/core/fullscreen_ui.cpp | 5 +- src/util/imgui_glyph_ranges.inl | 2 +- 5 files changed, 253 insertions(+), 78 deletions(-) diff --git a/dep/imgui/include/IconsEmoji.h b/dep/imgui/include/IconsEmoji.h index 9adfd39ef..cdae5fed5 100644 --- a/dep/imgui/include/IconsEmoji.h +++ b/dep/imgui/include/IconsEmoji.h @@ -29,3 +29,4 @@ #define ICON_EMOJI_UNLOCKED "\xf0\x9f\x94\x93" #define ICON_EMOJI_REFRESH "\xf0\x9f\x94\x84" #define ICON_EMOJI_PROHIBITED "\xf0\x9f\x9a\xab" +#define ICON_EMOJI_CALENDAR "\xF0\x9F\x93\x85" diff --git a/src/core/achievements.cpp b/src/core/achievements.cpp index a9da8ea40..5b3d687ee 100644 --- a/src/core/achievements.cpp +++ b/src/core/achievements.cpp @@ -155,6 +155,7 @@ static void BeginChangeDisc(); static void UpdateGameSummary(bool update_progress_database, bool force_update_progress_database); static std::string GetLocalImagePath(const std::string_view image_name, int type); static void DownloadImage(std::string url, std::string cache_path); +static const std::string& GetCachedAchievementBadgePath(const rc_client_achievement_t* achievement, int state); static void UpdateGlyphRanges(); static TinyString DecryptLoginToken(std::string_view encrypted_token, std::string_view username); @@ -266,7 +267,10 @@ struct State rc_client_async_handle_t* load_game_request = nullptr; rc_client_achievement_list_t* achievement_list = nullptr; - std::vector> achievement_badge_paths; + std::vector> achievement_badge_paths; + + const rc_client_achievement_t* most_recent_unlock = nullptr; + const rc_client_achievement_t* achievement_nearest_completion = nullptr; rc_client_leaderboard_list_t* leaderboard_list = nullptr; const rc_client_leaderboard_t* open_leaderboard = nullptr; @@ -1032,6 +1036,49 @@ void Achievements::UpdateGameSummary(bool update_progress_database, bool force_u UpdateProgressDatabase(force_update_progress_database); } +void Achievements::UpdateRecentUnlockAndAlmostThere() +{ + const auto lock = GetLock(); + if (!IsActive()) + return; + + s_state.most_recent_unlock = nullptr; + s_state.achievement_nearest_completion = nullptr; + + rc_client_achievement_list_t* const achievements = rc_client_create_achievement_list( + s_state.client, RC_CLIENT_ACHIEVEMENT_CATEGORY_CORE_AND_UNOFFICIAL, RC_CLIENT_ACHIEVEMENT_LIST_GROUPING_PROGRESS); + if (!achievements) + return; + + for (u32 i = 0; i < achievements->num_buckets; i++) + { + const rc_client_achievement_bucket_t& bucket = achievements->buckets[i]; + for (u32 j = 0; j < bucket.num_achievements; j++) + { + const rc_client_achievement_t* achievement = bucket.achievements[j]; + + if (achievement->state == RC_CLIENT_ACHIEVEMENT_STATE_UNLOCKED) + { + if (!s_state.most_recent_unlock || achievement->unlock_time > s_state.most_recent_unlock->unlock_time) + s_state.most_recent_unlock = achievement; + } + else + { + // find the achievement with the greatest normalized progress, but skip anything below 80%, + // matching the rc_client definition of "almost there" + const float percent_cutoff = 80.0f; + if (achievement->measured_percent >= percent_cutoff && + (!s_state.achievement_nearest_completion || + achievement->measured_percent > s_state.achievement_nearest_completion->measured_percent)) + { + s_state.achievement_nearest_completion = achievement; + } + } + } + } + rc_client_destroy_achievement_list(achievements); +} + void Achievements::UpdateRichPresence(std::unique_lock& lock) { // Limit rich presence updates to once per second, since it could change per frame. @@ -1973,6 +2020,18 @@ std::string Achievements::GetAchievementBadgePath(const rc_client_achievement_t* return path; } +const std::string& Achievements::GetCachedAchievementBadgePath(const rc_client_achievement_t* achievement, int state) +{ + for (const auto& [l_cheevo, l_state, l_path] : s_state.achievement_badge_paths) + { + if (l_cheevo == achievement && l_state == state) + return l_path; + } + + std::string path = GetAchievementBadgePath(achievement, state); + return std::get<2>(s_state.achievement_badge_paths.emplace_back(achievement, state, std::move(path))); +} + std::string Achievements::GetLeaderboardUserBadgePath(const rc_client_leaderboard_entry_t* entry) { // TODO: maybe we should just cache these in memory... @@ -2314,6 +2373,9 @@ void Achievements::ClearUIState() rc_client_destroy_achievement_list(s_state.achievement_list); s_state.achievement_list = nullptr; } + + s_state.most_recent_unlock = nullptr; + s_state.achievement_nearest_completion = nullptr; } template @@ -2485,83 +2547,203 @@ void Achievements::DrawGameOverlays() #ifndef __ANDROID__ -void Achievements::DrawPauseMenuOverlays() +void Achievements::DrawPauseMenuOverlays(float start_pos_y) { + using ImGuiFullscreen::DarkerColor; using ImGuiFullscreen::LayoutScale; + using ImGuiFullscreen::ModAlpha; using ImGuiFullscreen::UIStyle; - if (!HasActiveGame()) + if (!HasActiveGame() || !HasAchievements()) return; const auto lock = GetLock(); - if (s_state.active_challenge_indicators.empty() && !s_state.active_progress_indicator.has_value()) - return; + const ImVec2& display_size = ImGui::GetIO().DisplaySize; + const float box_margin = LayoutScale(20.0f); + const float box_width = LayoutScale(450.0f); + const float box_padding = LayoutScale(15.0f); + const float box_content_width = box_width - box_padding - box_padding; + const float box_rounding = LayoutScale(20.0f); + const u32 box_background_color = ImGui::GetColorU32(ModAlpha(UIStyle.BackgroundColor, 0.8f)); + const ImU32 title_text_color = ImGui::GetColorU32(UIStyle.BackgroundTextColor) | IM_COL32_A_MASK; + const ImU32 text_color = ImGui::GetColorU32(DarkerColor(UIStyle.BackgroundTextColor)) | IM_COL32_A_MASK; + const float paragraph_spacing = LayoutScale(10.0f); + const float text_spacing = LayoutScale(2.0f); - const ImGuiIO& io = ImGui::GetIO(); - ImFont* font = UIStyle.MediumFont; + const float progress_height = LayoutScale(20.0f); + const float badge_size = LayoutScale(40.0f); + const float badge_text_width = box_content_width - badge_size - text_spacing - text_spacing; - const ImVec2 image_size(LayoutScale(ImGuiFullscreen::LAYOUT_MENU_BUTTON_HEIGHT_NO_SUMMARY, - ImGuiFullscreen::LAYOUT_MENU_BUTTON_HEIGHT_NO_SUMMARY)); - const float start_y = - LayoutScale(10.0f + 4.0f + 4.0f) + UIStyle.LargeFont->FontSize + (UIStyle.MediumFont->FontSize * 2.0f); - const float margin = LayoutScale(10.0f); - const float spacing = LayoutScale(10.0f); - const float padding = LayoutScale(10.0f); + ImDrawList* dl = ImGui::GetBackgroundDrawList(); - const float max_text_width = ImGuiFullscreen::LayoutScale(300.0f); - const float row_width = max_text_width + padding + padding + image_size.x + spacing; - const float title_height = padding + font->FontSize + padding; + const auto get_achievement_height = [&badge_size, &badge_text_width, &text_spacing, &progress_height]( + const rc_client_achievement_t* achievement, bool show_measured) { + const ImVec2 description_size = UIStyle.MediumFont->CalcTextSizeA(UIStyle.MediumFont->FontSize, FLT_MAX, + badge_text_width, achievement->description); + float text_height = UIStyle.MediumFont->FontSize + text_spacing + description_size.y; +#if 0 + if (show_measured && achievement->measured_percent > 0.0f) + text_height += text_spacing + progress_height; +#endif + + return std::max(text_height, badge_size); + }; + + float box_height = + box_padding + box_padding + UIStyle.MediumFont->FontSize + paragraph_spacing + progress_height + paragraph_spacing; + if (s_state.most_recent_unlock) + { + box_height += UIStyle.MediumFont->FontSize + paragraph_spacing + + get_achievement_height(s_state.most_recent_unlock, false) + + (s_state.achievement_nearest_completion ? (paragraph_spacing + paragraph_spacing) : 0.0f); + } + if (s_state.achievement_nearest_completion) + { + box_height += UIStyle.MediumFont->FontSize + paragraph_spacing + + get_achievement_height(s_state.achievement_nearest_completion, true); + } + + ImVec2 box_min = ImVec2(display_size.x - box_width - box_margin, start_pos_y + box_margin); + ImVec2 box_max = ImVec2(box_min.x + box_width, box_min.y + box_height); + ImVec2 text_pos = ImVec2(box_min.x + box_padding, box_min.y + box_padding); + ImVec2 text_size; + + dl->AddRectFilled(box_min, box_max, box_background_color, box_rounding); + + const auto draw_achievement_with_summary = [&box_max, &badge_text_width, &dl, &title_text_color, &text_color, + &text_spacing, ¶graph_spacing, &text_pos, &progress_height, + &badge_size](const rc_client_achievement_t* achievement, + bool show_measured) { + const ImVec2 image_max = ImVec2(text_pos.x + badge_size, text_pos.y + badge_size); + ImVec2 badge_text_pos = ImVec2(image_max.x + text_spacing + text_spacing, text_pos.y); + const ImVec4 clip_rect = ImVec4(badge_text_pos.x, badge_text_pos.y, badge_text_pos.x + badge_text_width, box_max.y); + const ImVec2 description_size = UIStyle.MediumFont->CalcTextSizeA(UIStyle.MediumFont->FontSize, FLT_MAX, + badge_text_width, achievement->description); + + GPUTexture* badge_tex = ImGuiFullscreen::GetCachedTextureAsync( + GetCachedAchievementBadgePath(achievement, RC_CLIENT_ACHIEVEMENT_STATE_UNLOCKED)); + dl->AddImage(badge_tex, text_pos, image_max); + + dl->AddText(UIStyle.MediumFont, UIStyle.MediumFont->FontSize, badge_text_pos, title_text_color, achievement->title, + nullptr, 0.0f, &clip_rect); + badge_text_pos.y += UIStyle.MediumFont->FontSize + text_spacing; + + dl->AddText(UIStyle.MediumFont, UIStyle.MediumFont->FontSize, badge_text_pos, text_color, achievement->description, + nullptr, badge_text_width, &clip_rect); + badge_text_pos.y += description_size.y; + + if (show_measured && achievement->measured_percent > 0.0f) + { +#if 0 + // not a fan of the way this looks + badge_text_pos.y += text_spacing; + + const float progress_fraction = static_cast(achievement->measured_percent) / 100.0f; + const ImRect progress_bb(badge_text_pos, badge_text_pos + ImVec2(badge_text_width, progress_height)); + const u32 progress_color = ImGui::GetColorU32(DarkerColor(UIStyle.SecondaryColor)); + dl->AddRectFilled(progress_bb.Min, progress_bb.Max, ImGui::GetColorU32(UIStyle.PrimaryDarkColor)); + dl->AddRectFilled(progress_bb.Min, + ImVec2(progress_bb.Min.x + progress_fraction * progress_bb.GetWidth(), progress_bb.Max.y), + progress_color); + const ImVec2 text_size = + UIStyle.MediumFont->CalcTextSizeA(UIStyle.MediumFont->FontSize, FLT_MAX, 0.0f, achievement->measured_progress); + dl->AddText(UIStyle.MediumFont, UIStyle.MediumFont->FontSize, + ImVec2(progress_bb.Min.x + ((progress_bb.Max.x - progress_bb.Min.x) / 2.0f) - (text_size.x / 2.0f), + progress_bb.Min.y + ((progress_bb.Max.y - progress_bb.Min.y) / 2.0f) - (text_size.y / 2.0f)), + text_color, achievement->measured_progress); +#endif + } + + text_pos.y = badge_text_pos.y; + }; + + TinyString buffer; + + // title + { + dl->AddText(UIStyle.MediumFont, UIStyle.MediumFont->FontSize, text_pos, text_color, + TRANSLATE_DISAMBIG("Achievements", "Achievements Unlocked", "Pause Menu")); + const float unlocked_fraction = static_cast(s_state.game_summary.num_unlocked_achievements) / + static_cast(s_state.game_summary.num_core_achievements); + buffer.format("{}%", static_cast(std::ceil(unlocked_fraction * 100.0f))); + text_size = + UIStyle.MediumFont->CalcTextSizeA(UIStyle.MediumFont->FontSize, FLT_MAX, 0.0f, buffer.c_str(), buffer.end_ptr()); + dl->AddText(UIStyle.MediumFont, UIStyle.MediumFont->FontSize, + ImVec2(text_pos.x + (box_content_width - text_size.x), text_pos.y), text_color, buffer.c_str(), + buffer.end_ptr()); + text_pos.y += UIStyle.MediumFont->FontSize + paragraph_spacing; + + const ImRect progress_bb(text_pos, text_pos + ImVec2(box_content_width, progress_height)); + const u32 progress_color = ImGui::GetColorU32(DarkerColor(UIStyle.SecondaryColor)); + dl->AddRectFilled(progress_bb.Min, progress_bb.Max, ImGui::GetColorU32(UIStyle.PrimaryDarkColor)); + dl->AddRectFilled(progress_bb.Min, + ImVec2(progress_bb.Min.x + unlocked_fraction * progress_bb.GetWidth(), progress_bb.Max.y), + progress_color); + + buffer.format("{}/{}", s_state.game_summary.num_unlocked_achievements, s_state.game_summary.num_core_achievements); + text_size = + UIStyle.MediumFont->CalcTextSizeA(UIStyle.MediumFont->FontSize, FLT_MAX, 0.0f, buffer.c_str(), buffer.end_ptr()); + dl->AddText(UIStyle.MediumFont, UIStyle.MediumFont->FontSize, + ImVec2(progress_bb.Min.x + ((progress_bb.Max.x - progress_bb.Min.x) / 2.0f) - (text_size.x / 2.0f), + progress_bb.Min.y + ((progress_bb.Max.y - progress_bb.Min.y) / 2.0f) - (text_size.y / 2.0f)), + text_color, buffer.c_str(), buffer.end_ptr()); + text_pos.y += progress_height + paragraph_spacing; + } + + if (s_state.most_recent_unlock) + { + buffer.format(ICON_FA_LOCK_OPEN " {}", TRANSLATE_DISAMBIG_SV("Achievements", "Most Recent", "Pause Menu")); + dl->AddText(UIStyle.MediumFont, UIStyle.MediumFont->FontSize, text_pos, text_color, buffer.c_str(), + buffer.end_ptr()); + text_pos.y += UIStyle.MediumFont->FontSize + paragraph_spacing; + + draw_achievement_with_summary(s_state.most_recent_unlock, false); + + // extra spacing if we have two + text_pos.y += s_state.achievement_nearest_completion ? (paragraph_spacing + paragraph_spacing) : 0.0f; + } + + if (s_state.achievement_nearest_completion) + { + buffer.format(ICON_FA_LOCK " {}", TRANSLATE_DISAMBIG_SV("Achievements", "Nearest Completion", "Pause Menu")); + dl->AddText(UIStyle.MediumFont, UIStyle.MediumFont->FontSize, text_pos, text_color, buffer.c_str(), + buffer.end_ptr()); + text_pos.y += UIStyle.MediumFont->FontSize + paragraph_spacing; + + draw_achievement_with_summary(s_state.achievement_nearest_completion, true); + text_pos.y += paragraph_spacing; + } + + // Challenge indicators if (!s_state.active_challenge_indicators.empty()) { - const ImVec2 box_min(io.DisplaySize.x - row_width - margin, start_y + margin); - const ImVec2 box_max(box_min.x + row_width, - box_min.y + title_height + - (static_cast(s_state.active_challenge_indicators.size()) * (image_size.y + padding))); + box_height = box_padding + box_padding + UIStyle.MediumFont->FontSize; + for (size_t i = 0; i < s_state.active_challenge_indicators.size(); i++) + { + const AchievementChallengeIndicator& indicator = s_state.active_challenge_indicators[i]; + box_height += paragraph_spacing + get_achievement_height(indicator.achievement, false) + + ((i == s_state.active_challenge_indicators.size() - 1) ? paragraph_spacing : 0.0f); + } - ImDrawList* dl = ImGui::GetBackgroundDrawList(); - dl->AddRectFilled(box_min, box_max, IM_COL32(0x21, 0x21, 0x21, 200), LayoutScale(10.0f)); - dl->AddText(font, font->FontSize, ImVec2(box_min.x + padding, box_min.y + padding), IM_COL32(255, 255, 255, 255), - TRANSLATE("Achievements", "Active Challenge Achievements")); + box_min = ImVec2(box_min.x, box_max.y + box_margin); + box_max = ImVec2(box_min.x + box_width, box_min.y + box_height); + text_pos = ImVec2(box_min.x + box_padding, box_min.y + box_padding); - const float y_advance = image_size.y + spacing; - const float acheivement_name_offset = (image_size.y - font->FontSize) / 2.0f; - const float max_non_ellipised_text_width = max_text_width - LayoutScale(10.0f); - ImVec2 position(box_min.x + padding, box_min.y + title_height); + dl->AddRectFilled(box_min, box_max, box_background_color, box_rounding); + + buffer.format(ICON_FA_STOPWATCH " {}", + TRANSLATE_DISAMBIG_SV("Achievements", "Active Challenge Achievements", "Pause Menu")); + dl->AddText(UIStyle.MediumFont, UIStyle.MediumFont->FontSize, text_pos, text_color, buffer.c_str(), + buffer.end_ptr()); + text_pos.y += UIStyle.MediumFont->FontSize; for (const AchievementChallengeIndicator& indicator : s_state.active_challenge_indicators) { - GPUTexture* badge = ImGuiFullscreen::GetCachedTextureAsync(indicator.badge_path); - if (!badge) - continue; - - dl->AddImage(badge, position, position + image_size); - - const char* achievement_title = indicator.achievement->title; - const char* achievement_title_end = achievement_title + std::strlen(indicator.achievement->title); - const char* remaining_text = nullptr; - const ImVec2 text_width(font->CalcTextSizeA(font->FontSize, max_non_ellipised_text_width, 0.0f, achievement_title, - achievement_title_end, &remaining_text)); - const ImVec2 text_position(position.x + image_size.x + spacing, position.y + acheivement_name_offset); - const ImVec4 text_bbox(text_position.x, text_position.y, text_position.x + max_text_width, - text_position.y + image_size.y); - const u32 text_color = IM_COL32(255, 255, 255, 255); - - if (remaining_text < achievement_title_end) - { - dl->AddText(font, font->FontSize, text_position, text_color, achievement_title, remaining_text, 0.0f, - &text_bbox); - dl->AddText(font, font->FontSize, ImVec2(text_position.x + text_width.x, text_position.y), text_color, "...", - nullptr, 0.0f, &text_bbox); - } - else - { - dl->AddText(font, font->FontSize, text_position, text_color, achievement_title, achievement_title_end, 0.0f, - &text_bbox); - } - - position.y += y_advance; + text_pos.y += paragraph_spacing; + draw_achievement_with_summary(indicator.achievement, false); + text_pos.y += paragraph_spacing; } } } @@ -2826,24 +3008,13 @@ void Achievements::DrawAchievement(const rc_client_achievement_t* cheevo) if (!visible) return; - std::string* badge_path; - if (const auto badge_it = std::find_if(s_state.achievement_badge_paths.begin(), s_state.achievement_badge_paths.end(), - [cheevo](const auto& it) { return (it.first == cheevo); }); - badge_it != s_state.achievement_badge_paths.end()) - { - badge_path = &badge_it->second; - } - else - { - std::string new_badge_path = Achievements::GetAchievementBadgePath(cheevo, cheevo->state); - badge_path = &s_state.achievement_badge_paths.emplace_back(cheevo, std::move(new_badge_path)).second; - } + const std::string& badge_path = GetCachedAchievementBadgePath(cheevo, cheevo->state); const ImVec2 image_size( LayoutScale(ImGuiFullscreen::LAYOUT_MENU_BUTTON_HEIGHT, ImGuiFullscreen::LAYOUT_MENU_BUTTON_HEIGHT)); - if (!badge_path->empty()) + if (!badge_path.empty()) { - GPUTexture* badge = ImGuiFullscreen::GetCachedTextureAsync(*badge_path); + GPUTexture* badge = ImGuiFullscreen::GetCachedTextureAsync(badge_path); if (badge) { ImGui::GetWindowDrawList()->AddImage(badge, bb.Min, bb.Min + image_size, ImVec2(0.0f, 0.0f), ImVec2(1.0f, 1.0f), diff --git a/src/core/achievements.h b/src/core/achievements.h index 51eb33fb1..72e509310 100644 --- a/src/core/achievements.h +++ b/src/core/achievements.h @@ -182,7 +182,11 @@ void ClearUIState(); void DrawGameOverlays(); /// Draws ImGui overlays when paused. -void DrawPauseMenuOverlays(); +void DrawPauseMenuOverlays(float start_pos_y); + +/// Updates the stored most-recent and closest-to-completion achievements. +/// Call before calling DrawPauseMenuOverlays() for the first time. +void UpdateRecentUnlockAndAlmostThere(); #ifndef __ANDROID__ diff --git a/src/core/fullscreen_ui.cpp b/src/core/fullscreen_ui.cpp index c974c3d19..9e3643b39 100644 --- a/src/core/fullscreen_ui.cpp +++ b/src/core/fullscreen_ui.cpp @@ -821,6 +821,7 @@ void FullscreenUI::OpenPauseMenu() return; PauseForMenuOpen(true); + Achievements::UpdateRecentUnlockAndAlmostThere(); s_state.current_main_window = MainWindowType::PauseMenu; s_state.current_pause_submenu = PauseSubMenu::None; QueueResetFocus(FocusResetType::ViewChanged); @@ -6493,7 +6494,7 @@ void FullscreenUI::DrawPauseMenu() ImVec2(display_size.x, display_size.x - scaled_top_bar_height - LayoutScale(LAYOUT_FOOTER_HEIGHT)), ImGui::GetColorU32(ModAlpha(UIStyle.BackgroundColor, 0.85f))); - Achievements::DrawPauseMenuOverlays(); + Achievements::DrawPauseMenuOverlays(scaled_top_bar_height); if (BeginFullscreenWindow(window_pos, window_size, "pause_menu", ImVec4(0.0f, 0.0f, 0.0f, 0.0f), 0.0f, ImVec2(10.0f, 10.0f), ImGuiWindowFlags_NoBackground)) @@ -6613,8 +6614,6 @@ void FullscreenUI::DrawPauseMenu() EndFullscreenWindow(); } - Achievements::DrawPauseMenuOverlays(); - if (IsGamepadInputSource()) { SetFullscreenFooterText(std::array{std::make_pair(ICON_PF_XBOX_DPAD_UP_DOWN, FSUI_VSTR("Change Selection")), diff --git a/src/util/imgui_glyph_ranges.inl b/src/util/imgui_glyph_ranges.inl index dc008a99b..ad2a8b8b6 100644 --- a/src/util/imgui_glyph_ranges.inl +++ b/src/util/imgui_glyph_ranges.inl @@ -1,7 +1,7 @@ // SPDX-FileCopyrightText: 2019-2024 Connor McLaughlin // SPDX-License-Identifier: CC-BY-NC-ND-4.0 -static constexpr ImWchar FA_ICON_RANGE[] = { 0xe06f,0xe070,0xe086,0xe086,0xf002,0xf002,0xf005,0xf005,0xf007,0xf007,0xf00c,0xf00e,0xf011,0xf013,0xf017,0xf017,0xf019,0xf019,0xf01c,0xf01c,0xf021,0xf021,0xf023,0xf023,0xf025,0xf026,0xf028,0xf028,0xf02e,0xf02e,0xf030,0xf030,0xf03a,0xf03a,0xf03d,0xf03e,0xf04a,0xf04c,0xf050,0xf050,0xf056,0xf056,0xf059,0xf059,0xf05e,0xf05e,0xf062,0xf063,0xf065,0xf067,0xf071,0xf071,0xf075,0xf075,0xf077,0xf078,0xf07b,0xf07c,0xf083,0xf085,0xf091,0xf091,0xf09c,0xf09c,0xf0ac,0xf0ae,0xf0b2,0xf0b2,0xf0c3,0xf0c3,0xf0c5,0xf0c5,0xf0c7,0xf0c9,0xf0cb,0xf0cb,0xf0d0,0xf0d0,0xf0dc,0xf0dc,0xf0e0,0xf0e0,0xf0e2,0xf0e2,0xf0e7,0xf0e8,0xf0eb,0xf0eb,0xf0f1,0xf0f1,0xf0f3,0xf0f3,0xf0fe,0xf0fe,0xf110,0xf110,0xf11b,0xf11c,0xf140,0xf140,0xf144,0xf144,0xf146,0xf146,0xf14a,0xf14a,0xf15b,0xf15d,0xf191,0xf192,0xf1ab,0xf1ab,0xf1c0,0xf1c0,0xf1c5,0xf1c5,0xf1de,0xf1de,0xf1e6,0xf1e6,0xf1eb,0xf1eb,0xf1f8,0xf1f8,0xf1fb,0xf1fc,0xf201,0xf201,0xf240,0xf240,0xf242,0xf242,0xf245,0xf245,0xf26c,0xf26c,0xf279,0xf279,0xf2c1,0xf2c1,0xf2d0,0xf2d0,0xf2db,0xf2db,0xf2f1,0xf2f2,0xf302,0xf302,0xf31e,0xf31e,0xf35d,0xf35d,0xf360,0xf360,0xf362,0xf362,0xf3fd,0xf3fd,0xf410,0xf410,0xf422,0xf422,0xf424,0xf424,0xf462,0xf462,0xf466,0xf466,0xf4ce,0xf4ce,0xf500,0xf500,0xf517,0xf517,0xf51f,0xf51f,0xf538,0xf538,0xf53f,0xf53f,0xf545,0xf545,0xf547,0xf548,0xf54c,0xf54c,0xf55b,0xf55b,0xf55d,0xf55d,0xf565,0xf565,0xf56e,0xf570,0xf575,0xf575,0xf5a2,0xf5a2,0xf5aa,0xf5aa,0xf5ae,0xf5ae,0xf5c7,0xf5c7,0xf5cb,0xf5cb,0xf5e7,0xf5e7,0xf5ee,0xf5ee,0xf61f,0xf61f,0xf65d,0xf65e,0xf6a9,0xf6a9,0xf6cf,0xf6cf,0xf70c,0xf70c,0xf70e,0xf70e,0xf78c,0xf78c,0xf794,0xf794,0xf7a0,0xf7a0,0xf7a4,0xf7a5,0xf7c2,0xf7c2,0xf807,0xf807,0xf815,0xf815,0xf818,0xf818,0xf84c,0xf84c,0xf853,0xf853,0xf87d,0xf87d,0xf8cc,0xf8cc,0x0,0x0 }; +static constexpr ImWchar FA_ICON_RANGE[] = { 0xe06f,0xe070,0xe086,0xe086,0xf002,0xf002,0xf005,0xf005,0xf007,0xf007,0xf00c,0xf00e,0xf011,0xf013,0xf017,0xf017,0xf019,0xf019,0xf01c,0xf01c,0xf021,0xf021,0xf023,0xf023,0xf025,0xf026,0xf028,0xf028,0xf02e,0xf02e,0xf030,0xf030,0xf03a,0xf03a,0xf03d,0xf03e,0xf04a,0xf04c,0xf050,0xf050,0xf056,0xf056,0xf059,0xf059,0xf05e,0xf05e,0xf062,0xf063,0xf065,0xf067,0xf071,0xf071,0xf075,0xf075,0xf077,0xf078,0xf07b,0xf07c,0xf083,0xf085,0xf091,0xf091,0xf09c,0xf09c,0xf0ac,0xf0ae,0xf0b2,0xf0b2,0xf0c3,0xf0c3,0xf0c5,0xf0c5,0xf0c7,0xf0c9,0xf0cb,0xf0cb,0xf0d0,0xf0d0,0xf0dc,0xf0dc,0xf0e0,0xf0e0,0xf0e2,0xf0e2,0xf0e7,0xf0e8,0xf0eb,0xf0eb,0xf0f1,0xf0f1,0xf0f3,0xf0f3,0xf0fe,0xf0fe,0xf110,0xf110,0xf11b,0xf11c,0xf140,0xf140,0xf144,0xf144,0xf146,0xf146,0xf14a,0xf14a,0xf15b,0xf15d,0xf191,0xf192,0xf1ab,0xf1ab,0xf1c0,0xf1c0,0xf1c5,0xf1c5,0xf1de,0xf1de,0xf1e6,0xf1e6,0xf1eb,0xf1eb,0xf1f8,0xf1f8,0xf1fb,0xf1fc,0xf201,0xf201,0xf240,0xf240,0xf242,0xf242,0xf245,0xf245,0xf26c,0xf26c,0xf279,0xf279,0xf2c1,0xf2c1,0xf2d0,0xf2d0,0xf2db,0xf2db,0xf2f1,0xf2f2,0xf302,0xf302,0xf31e,0xf31e,0xf35d,0xf35d,0xf360,0xf360,0xf362,0xf362,0xf3c1,0xf3c1,0xf3fd,0xf3fd,0xf410,0xf410,0xf422,0xf422,0xf424,0xf424,0xf462,0xf462,0xf466,0xf466,0xf4ce,0xf4ce,0xf500,0xf500,0xf517,0xf517,0xf51f,0xf51f,0xf538,0xf538,0xf53f,0xf53f,0xf545,0xf545,0xf547,0xf548,0xf54c,0xf54c,0xf55b,0xf55b,0xf55d,0xf55d,0xf565,0xf565,0xf56e,0xf570,0xf575,0xf575,0xf5a2,0xf5a2,0xf5aa,0xf5aa,0xf5ae,0xf5ae,0xf5c7,0xf5c7,0xf5cb,0xf5cb,0xf5e7,0xf5e7,0xf5ee,0xf5ee,0xf61f,0xf61f,0xf65d,0xf65e,0xf6a9,0xf6a9,0xf6cf,0xf6cf,0xf70c,0xf70c,0xf70e,0xf70e,0xf78c,0xf78c,0xf794,0xf794,0xf7a0,0xf7a0,0xf7a4,0xf7a5,0xf7c2,0xf7c2,0xf807,0xf807,0xf815,0xf815,0xf818,0xf818,0xf84c,0xf84c,0xf853,0xf853,0xf87d,0xf87d,0xf8cc,0xf8cc,0x0,0x0 }; static constexpr ImWchar PF_ICON_RANGE[] = { 0x2196,0x2199,0x219e,0x21a3,0x21b0,0x21b3,0x21ba,0x21c3,0x21c7,0x21ca,0x21d0,0x21d4,0x21e0,0x21e3,0x21e6,0x21e8,0x21eb,0x21eb,0x21ed,0x21ee,0x21f7,0x21f8,0x21fa,0x21fb,0x221a,0x221b,0x227a,0x227f,0x2284,0x2284,0x22bf,0x22c8,0x2349,0x2349,0x235e,0x235e,0x2360,0x2361,0x2364,0x2366,0x23b2,0x23b4,0x23cc,0x23cc,0x23ce,0x23ce,0x23f4,0x23f7,0x2427,0x243a,0x243c,0x243e,0x2446,0x2446,0x2460,0x246b,0x248f,0x248f,0x24f5,0x24fd,0x24ff,0x24ff,0x2717,0x2717,0x2753,0x2753,0x278a,0x278e,0x27fc,0x27fc,0xe000,0xe001,0xff21,0xff3a,0x1f52b,0x1f52b,0x0,0x0 };