diff --git a/data/resources/address-book-new.png b/data/resources/address-book-new.png new file mode 100644 index 000000000..bb6bffc16 Binary files /dev/null and b/data/resources/address-book-new.png differ diff --git a/data/resources/applications-system.png b/data/resources/applications-system.png new file mode 100644 index 000000000..2007dbb7d Binary files /dev/null and b/data/resources/applications-system.png differ diff --git a/data/resources/duck.png b/data/resources/duck.png new file mode 100644 index 000000000..8cf01c44c Binary files /dev/null and b/data/resources/duck.png differ diff --git a/data/resources/fa-solid-900.ttf b/data/resources/fa-solid-900.ttf new file mode 100644 index 000000000..49133379e Binary files /dev/null and b/data/resources/fa-solid-900.ttf differ diff --git a/data/resources/flag-eu.png b/data/resources/flag-eu.png new file mode 100644 index 000000000..cfbdd67c8 Binary files /dev/null and b/data/resources/flag-eu.png differ diff --git a/data/resources/flag-jp.png b/data/resources/flag-jp.png new file mode 100644 index 000000000..80c82bff1 Binary files /dev/null and b/data/resources/flag-jp.png differ diff --git a/data/resources/flag-uc.png b/data/resources/flag-uc.png new file mode 100644 index 000000000..f143e2099 Binary files /dev/null and b/data/resources/flag-uc.png differ diff --git a/data/resources/media-cdrom.png b/data/resources/media-cdrom.png new file mode 100644 index 000000000..a85334c7e Binary files /dev/null and b/data/resources/media-cdrom.png differ diff --git a/data/resources/multimedia-player.png b/data/resources/multimedia-player.png new file mode 100644 index 000000000..ea5e24894 Binary files /dev/null and b/data/resources/multimedia-player.png differ diff --git a/data/resources/star-0.png b/data/resources/star-0.png new file mode 100644 index 000000000..e5b56db70 Binary files /dev/null and b/data/resources/star-0.png differ diff --git a/data/resources/star-1.png b/data/resources/star-1.png new file mode 100644 index 000000000..ae91a29ac Binary files /dev/null and b/data/resources/star-1.png differ diff --git a/data/resources/star-2.png b/data/resources/star-2.png new file mode 100644 index 000000000..f7bee9b1d Binary files /dev/null and b/data/resources/star-2.png differ diff --git a/data/resources/star-3.png b/data/resources/star-3.png new file mode 100644 index 000000000..330aefbac Binary files /dev/null and b/data/resources/star-3.png differ diff --git a/data/resources/star-4.png b/data/resources/star-4.png new file mode 100644 index 000000000..4e9f58dfa Binary files /dev/null and b/data/resources/star-4.png differ diff --git a/data/resources/star-5.png b/data/resources/star-5.png new file mode 100644 index 000000000..aa5707ea7 Binary files /dev/null and b/data/resources/star-5.png differ diff --git a/src/frontend-common/CMakeLists.txt b/src/frontend-common/CMakeLists.txt index a13d148e9..29da659b7 100644 --- a/src/frontend-common/CMakeLists.txt +++ b/src/frontend-common/CMakeLists.txt @@ -5,6 +5,10 @@ add_library(frontend-common controller_interface.h cubeb_audio_stream.cpp cubeb_audio_stream.h + fullscreen_ui.cpp + fullscreen_ui.h + fullscreen_ui_progress_callback.cpp + fullscreen_ui_progress_callback.h game_list.cpp game_list.h game_settings.cpp @@ -13,6 +17,8 @@ add_library(frontend-common icon.h ini_settings_interface.cpp ini_settings_interface.h + imgui_fullscreen.h + imgui_fullscreen.cpp imgui_impl_opengl3.cpp imgui_impl_opengl3.h imgui_impl_vulkan.cpp diff --git a/src/frontend-common/IconsFontAwesome5.h b/src/frontend-common/IconsFontAwesome5.h new file mode 100644 index 000000000..67cddab50 --- /dev/null +++ b/src/frontend-common/IconsFontAwesome5.h @@ -0,0 +1,1012 @@ +// Generated by https://github.com/juliettef/IconFontCppHeaders script GenerateIconFontCppHeaders.py for languages C and C++ +// from https://raw.githubusercontent.com/FortAwesome/Font-Awesome/master/metadata/icons.yml +// for use with https://github.com/FortAwesome/Font-Awesome/blob/master/webfonts/fa-regular-400.ttf, https://github.com/FortAwesome/Font-Awesome/blob/master/webfonts/fa-solid-900.ttf +#pragma once + +#define FONT_ICON_FILE_NAME_FAR "fa-regular-400.ttf" +#define FONT_ICON_FILE_NAME_FAS "fa-solid-900.ttf" + +#define ICON_MIN_FA 0xe005 +#define ICON_MAX_FA 0xf8ff +#define ICON_FA_AD "\xef\x99\x81" // U+f641 +#define ICON_FA_ADDRESS_BOOK "\xef\x8a\xb9" // U+f2b9 +#define ICON_FA_ADDRESS_CARD "\xef\x8a\xbb" // U+f2bb +#define ICON_FA_ADJUST "\xef\x81\x82" // U+f042 +#define ICON_FA_AIR_FRESHENER "\xef\x97\x90" // U+f5d0 +#define ICON_FA_ALIGN_CENTER "\xef\x80\xb7" // U+f037 +#define ICON_FA_ALIGN_JUSTIFY "\xef\x80\xb9" // U+f039 +#define ICON_FA_ALIGN_LEFT "\xef\x80\xb6" // U+f036 +#define ICON_FA_ALIGN_RIGHT "\xef\x80\xb8" // U+f038 +#define ICON_FA_ALLERGIES "\xef\x91\xa1" // U+f461 +#define ICON_FA_AMBULANCE "\xef\x83\xb9" // U+f0f9 +#define ICON_FA_AMERICAN_SIGN_LANGUAGE_INTERPRETING "\xef\x8a\xa3" // U+f2a3 +#define ICON_FA_ANCHOR "\xef\x84\xbd" // U+f13d +#define ICON_FA_ANGLE_DOUBLE_DOWN "\xef\x84\x83" // U+f103 +#define ICON_FA_ANGLE_DOUBLE_LEFT "\xef\x84\x80" // U+f100 +#define ICON_FA_ANGLE_DOUBLE_RIGHT "\xef\x84\x81" // U+f101 +#define ICON_FA_ANGLE_DOUBLE_UP "\xef\x84\x82" // U+f102 +#define ICON_FA_ANGLE_DOWN "\xef\x84\x87" // U+f107 +#define ICON_FA_ANGLE_LEFT "\xef\x84\x84" // U+f104 +#define ICON_FA_ANGLE_RIGHT "\xef\x84\x85" // U+f105 +#define ICON_FA_ANGLE_UP "\xef\x84\x86" // U+f106 +#define ICON_FA_ANGRY "\xef\x95\x96" // U+f556 +#define ICON_FA_ANKH "\xef\x99\x84" // U+f644 +#define ICON_FA_APPLE_ALT "\xef\x97\x91" // U+f5d1 +#define ICON_FA_ARCHIVE "\xef\x86\x87" // U+f187 +#define ICON_FA_ARCHWAY "\xef\x95\x97" // U+f557 +#define ICON_FA_ARROW_ALT_CIRCLE_DOWN "\xef\x8d\x98" // U+f358 +#define ICON_FA_ARROW_ALT_CIRCLE_LEFT "\xef\x8d\x99" // U+f359 +#define ICON_FA_ARROW_ALT_CIRCLE_RIGHT "\xef\x8d\x9a" // U+f35a +#define ICON_FA_ARROW_ALT_CIRCLE_UP "\xef\x8d\x9b" // U+f35b +#define ICON_FA_ARROW_CIRCLE_DOWN "\xef\x82\xab" // U+f0ab +#define ICON_FA_ARROW_CIRCLE_LEFT "\xef\x82\xa8" // U+f0a8 +#define ICON_FA_ARROW_CIRCLE_RIGHT "\xef\x82\xa9" // U+f0a9 +#define ICON_FA_ARROW_CIRCLE_UP "\xef\x82\xaa" // U+f0aa +#define ICON_FA_ARROW_DOWN "\xef\x81\xa3" // U+f063 +#define ICON_FA_ARROW_LEFT "\xef\x81\xa0" // U+f060 +#define ICON_FA_ARROW_RIGHT "\xef\x81\xa1" // U+f061 +#define ICON_FA_ARROW_UP "\xef\x81\xa2" // U+f062 +#define ICON_FA_ARROWS_ALT "\xef\x82\xb2" // U+f0b2 +#define ICON_FA_ARROWS_ALT_H "\xef\x8c\xb7" // U+f337 +#define ICON_FA_ARROWS_ALT_V "\xef\x8c\xb8" // U+f338 +#define ICON_FA_ASSISTIVE_LISTENING_SYSTEMS "\xef\x8a\xa2" // U+f2a2 +#define ICON_FA_ASTERISK "\xef\x81\xa9" // U+f069 +#define ICON_FA_AT "\xef\x87\xba" // U+f1fa +#define ICON_FA_ATLAS "\xef\x95\x98" // U+f558 +#define ICON_FA_ATOM "\xef\x97\x92" // U+f5d2 +#define ICON_FA_AUDIO_DESCRIPTION "\xef\x8a\x9e" // U+f29e +#define ICON_FA_AWARD "\xef\x95\x99" // U+f559 +#define ICON_FA_BABY "\xef\x9d\xbc" // U+f77c +#define ICON_FA_BABY_CARRIAGE "\xef\x9d\xbd" // U+f77d +#define ICON_FA_BACKSPACE "\xef\x95\x9a" // U+f55a +#define ICON_FA_BACKWARD "\xef\x81\x8a" // U+f04a +#define ICON_FA_BACON "\xef\x9f\xa5" // U+f7e5 +#define ICON_FA_BACTERIA "\xee\x81\x99" // U+e059 +#define ICON_FA_BACTERIUM "\xee\x81\x9a" // U+e05a +#define ICON_FA_BAHAI "\xef\x99\xa6" // U+f666 +#define ICON_FA_BALANCE_SCALE "\xef\x89\x8e" // U+f24e +#define ICON_FA_BALANCE_SCALE_LEFT "\xef\x94\x95" // U+f515 +#define ICON_FA_BALANCE_SCALE_RIGHT "\xef\x94\x96" // U+f516 +#define ICON_FA_BAN "\xef\x81\x9e" // U+f05e +#define ICON_FA_BAND_AID "\xef\x91\xa2" // U+f462 +#define ICON_FA_BARCODE "\xef\x80\xaa" // U+f02a +#define ICON_FA_BARS "\xef\x83\x89" // U+f0c9 +#define ICON_FA_BASEBALL_BALL "\xef\x90\xb3" // U+f433 +#define ICON_FA_BASKETBALL_BALL "\xef\x90\xb4" // U+f434 +#define ICON_FA_BATH "\xef\x8b\x8d" // U+f2cd +#define ICON_FA_BATTERY_EMPTY "\xef\x89\x84" // U+f244 +#define ICON_FA_BATTERY_FULL "\xef\x89\x80" // U+f240 +#define ICON_FA_BATTERY_HALF "\xef\x89\x82" // U+f242 +#define ICON_FA_BATTERY_QUARTER "\xef\x89\x83" // U+f243 +#define ICON_FA_BATTERY_THREE_QUARTERS "\xef\x89\x81" // U+f241 +#define ICON_FA_BED "\xef\x88\xb6" // U+f236 +#define ICON_FA_BEER "\xef\x83\xbc" // U+f0fc +#define ICON_FA_BELL "\xef\x83\xb3" // U+f0f3 +#define ICON_FA_BELL_SLASH "\xef\x87\xb6" // U+f1f6 +#define ICON_FA_BEZIER_CURVE "\xef\x95\x9b" // U+f55b +#define ICON_FA_BIBLE "\xef\x99\x87" // U+f647 +#define ICON_FA_BICYCLE "\xef\x88\x86" // U+f206 +#define ICON_FA_BIKING "\xef\xa1\x8a" // U+f84a +#define ICON_FA_BINOCULARS "\xef\x87\xa5" // U+f1e5 +#define ICON_FA_BIOHAZARD "\xef\x9e\x80" // U+f780 +#define ICON_FA_BIRTHDAY_CAKE "\xef\x87\xbd" // U+f1fd +#define ICON_FA_BLENDER "\xef\x94\x97" // U+f517 +#define ICON_FA_BLENDER_PHONE "\xef\x9a\xb6" // U+f6b6 +#define ICON_FA_BLIND "\xef\x8a\x9d" // U+f29d +#define ICON_FA_BLOG "\xef\x9e\x81" // U+f781 +#define ICON_FA_BOLD "\xef\x80\xb2" // U+f032 +#define ICON_FA_BOLT "\xef\x83\xa7" // U+f0e7 +#define ICON_FA_BOMB "\xef\x87\xa2" // U+f1e2 +#define ICON_FA_BONE "\xef\x97\x97" // U+f5d7 +#define ICON_FA_BONG "\xef\x95\x9c" // U+f55c +#define ICON_FA_BOOK "\xef\x80\xad" // U+f02d +#define ICON_FA_BOOK_DEAD "\xef\x9a\xb7" // U+f6b7 +#define ICON_FA_BOOK_MEDICAL "\xef\x9f\xa6" // U+f7e6 +#define ICON_FA_BOOK_OPEN "\xef\x94\x98" // U+f518 +#define ICON_FA_BOOK_READER "\xef\x97\x9a" // U+f5da +#define ICON_FA_BOOKMARK "\xef\x80\xae" // U+f02e +#define ICON_FA_BORDER_ALL "\xef\xa1\x8c" // U+f84c +#define ICON_FA_BORDER_NONE "\xef\xa1\x90" // U+f850 +#define ICON_FA_BORDER_STYLE "\xef\xa1\x93" // U+f853 +#define ICON_FA_BOWLING_BALL "\xef\x90\xb6" // U+f436 +#define ICON_FA_BOX "\xef\x91\xa6" // U+f466 +#define ICON_FA_BOX_OPEN "\xef\x92\x9e" // U+f49e +#define ICON_FA_BOX_TISSUE "\xee\x81\x9b" // U+e05b +#define ICON_FA_BOXES "\xef\x91\xa8" // U+f468 +#define ICON_FA_BRAILLE "\xef\x8a\xa1" // U+f2a1 +#define ICON_FA_BRAIN "\xef\x97\x9c" // U+f5dc +#define ICON_FA_BREAD_SLICE "\xef\x9f\xac" // U+f7ec +#define ICON_FA_BRIEFCASE "\xef\x82\xb1" // U+f0b1 +#define ICON_FA_BRIEFCASE_MEDICAL "\xef\x91\xa9" // U+f469 +#define ICON_FA_BROADCAST_TOWER "\xef\x94\x99" // U+f519 +#define ICON_FA_BROOM "\xef\x94\x9a" // U+f51a +#define ICON_FA_BRUSH "\xef\x95\x9d" // U+f55d +#define ICON_FA_BUG "\xef\x86\x88" // U+f188 +#define ICON_FA_BUILDING "\xef\x86\xad" // U+f1ad +#define ICON_FA_BULLHORN "\xef\x82\xa1" // U+f0a1 +#define ICON_FA_BULLSEYE "\xef\x85\x80" // U+f140 +#define ICON_FA_BURN "\xef\x91\xaa" // U+f46a +#define ICON_FA_BUS "\xef\x88\x87" // U+f207 +#define ICON_FA_BUS_ALT "\xef\x95\x9e" // U+f55e +#define ICON_FA_BUSINESS_TIME "\xef\x99\x8a" // U+f64a +#define ICON_FA_CALCULATOR "\xef\x87\xac" // U+f1ec +#define ICON_FA_CALENDAR "\xef\x84\xb3" // U+f133 +#define ICON_FA_CALENDAR_ALT "\xef\x81\xb3" // U+f073 +#define ICON_FA_CALENDAR_CHECK "\xef\x89\xb4" // U+f274 +#define ICON_FA_CALENDAR_DAY "\xef\x9e\x83" // U+f783 +#define ICON_FA_CALENDAR_MINUS "\xef\x89\xb2" // U+f272 +#define ICON_FA_CALENDAR_PLUS "\xef\x89\xb1" // U+f271 +#define ICON_FA_CALENDAR_TIMES "\xef\x89\xb3" // U+f273 +#define ICON_FA_CALENDAR_WEEK "\xef\x9e\x84" // U+f784 +#define ICON_FA_CAMERA "\xef\x80\xb0" // U+f030 +#define ICON_FA_CAMERA_RETRO "\xef\x82\x83" // U+f083 +#define ICON_FA_CAMPGROUND "\xef\x9a\xbb" // U+f6bb +#define ICON_FA_CANDY_CANE "\xef\x9e\x86" // U+f786 +#define ICON_FA_CANNABIS "\xef\x95\x9f" // U+f55f +#define ICON_FA_CAPSULES "\xef\x91\xab" // U+f46b +#define ICON_FA_CAR "\xef\x86\xb9" // U+f1b9 +#define ICON_FA_CAR_ALT "\xef\x97\x9e" // U+f5de +#define ICON_FA_CAR_BATTERY "\xef\x97\x9f" // U+f5df +#define ICON_FA_CAR_CRASH "\xef\x97\xa1" // U+f5e1 +#define ICON_FA_CAR_SIDE "\xef\x97\xa4" // U+f5e4 +#define ICON_FA_CARAVAN "\xef\xa3\xbf" // U+f8ff +#define ICON_FA_CARET_DOWN "\xef\x83\x97" // U+f0d7 +#define ICON_FA_CARET_LEFT "\xef\x83\x99" // U+f0d9 +#define ICON_FA_CARET_RIGHT "\xef\x83\x9a" // U+f0da +#define ICON_FA_CARET_SQUARE_DOWN "\xef\x85\x90" // U+f150 +#define ICON_FA_CARET_SQUARE_LEFT "\xef\x86\x91" // U+f191 +#define ICON_FA_CARET_SQUARE_RIGHT "\xef\x85\x92" // U+f152 +#define ICON_FA_CARET_SQUARE_UP "\xef\x85\x91" // U+f151 +#define ICON_FA_CARET_UP "\xef\x83\x98" // U+f0d8 +#define ICON_FA_CARROT "\xef\x9e\x87" // U+f787 +#define ICON_FA_CART_ARROW_DOWN "\xef\x88\x98" // U+f218 +#define ICON_FA_CART_PLUS "\xef\x88\x97" // U+f217 +#define ICON_FA_CASH_REGISTER "\xef\x9e\x88" // U+f788 +#define ICON_FA_CAT "\xef\x9a\xbe" // U+f6be +#define ICON_FA_CERTIFICATE "\xef\x82\xa3" // U+f0a3 +#define ICON_FA_CHAIR "\xef\x9b\x80" // U+f6c0 +#define ICON_FA_CHALKBOARD "\xef\x94\x9b" // U+f51b +#define ICON_FA_CHALKBOARD_TEACHER "\xef\x94\x9c" // U+f51c +#define ICON_FA_CHARGING_STATION "\xef\x97\xa7" // U+f5e7 +#define ICON_FA_CHART_AREA "\xef\x87\xbe" // U+f1fe +#define ICON_FA_CHART_BAR "\xef\x82\x80" // U+f080 +#define ICON_FA_CHART_LINE "\xef\x88\x81" // U+f201 +#define ICON_FA_CHART_PIE "\xef\x88\x80" // U+f200 +#define ICON_FA_CHECK "\xef\x80\x8c" // U+f00c +#define ICON_FA_CHECK_CIRCLE "\xef\x81\x98" // U+f058 +#define ICON_FA_CHECK_DOUBLE "\xef\x95\xa0" // U+f560 +#define ICON_FA_CHECK_SQUARE "\xef\x85\x8a" // U+f14a +#define ICON_FA_CHEESE "\xef\x9f\xaf" // U+f7ef +#define ICON_FA_CHESS "\xef\x90\xb9" // U+f439 +#define ICON_FA_CHESS_BISHOP "\xef\x90\xba" // U+f43a +#define ICON_FA_CHESS_BOARD "\xef\x90\xbc" // U+f43c +#define ICON_FA_CHESS_KING "\xef\x90\xbf" // U+f43f +#define ICON_FA_CHESS_KNIGHT "\xef\x91\x81" // U+f441 +#define ICON_FA_CHESS_PAWN "\xef\x91\x83" // U+f443 +#define ICON_FA_CHESS_QUEEN "\xef\x91\x85" // U+f445 +#define ICON_FA_CHESS_ROOK "\xef\x91\x87" // U+f447 +#define ICON_FA_CHEVRON_CIRCLE_DOWN "\xef\x84\xba" // U+f13a +#define ICON_FA_CHEVRON_CIRCLE_LEFT "\xef\x84\xb7" // U+f137 +#define ICON_FA_CHEVRON_CIRCLE_RIGHT "\xef\x84\xb8" // U+f138 +#define ICON_FA_CHEVRON_CIRCLE_UP "\xef\x84\xb9" // U+f139 +#define ICON_FA_CHEVRON_DOWN "\xef\x81\xb8" // U+f078 +#define ICON_FA_CHEVRON_LEFT "\xef\x81\x93" // U+f053 +#define ICON_FA_CHEVRON_RIGHT "\xef\x81\x94" // U+f054 +#define ICON_FA_CHEVRON_UP "\xef\x81\xb7" // U+f077 +#define ICON_FA_CHILD "\xef\x86\xae" // U+f1ae +#define ICON_FA_CHURCH "\xef\x94\x9d" // U+f51d +#define ICON_FA_CIRCLE "\xef\x84\x91" // U+f111 +#define ICON_FA_CIRCLE_NOTCH "\xef\x87\x8e" // U+f1ce +#define ICON_FA_CITY "\xef\x99\x8f" // U+f64f +#define ICON_FA_CLINIC_MEDICAL "\xef\x9f\xb2" // U+f7f2 +#define ICON_FA_CLIPBOARD "\xef\x8c\xa8" // U+f328 +#define ICON_FA_CLIPBOARD_CHECK "\xef\x91\xac" // U+f46c +#define ICON_FA_CLIPBOARD_LIST "\xef\x91\xad" // U+f46d +#define ICON_FA_CLOCK "\xef\x80\x97" // U+f017 +#define ICON_FA_CLONE "\xef\x89\x8d" // U+f24d +#define ICON_FA_CLOSED_CAPTIONING "\xef\x88\x8a" // U+f20a +#define ICON_FA_CLOUD "\xef\x83\x82" // U+f0c2 +#define ICON_FA_CLOUD_DOWNLOAD_ALT "\xef\x8e\x81" // U+f381 +#define ICON_FA_CLOUD_MEATBALL "\xef\x9c\xbb" // U+f73b +#define ICON_FA_CLOUD_MOON "\xef\x9b\x83" // U+f6c3 +#define ICON_FA_CLOUD_MOON_RAIN "\xef\x9c\xbc" // U+f73c +#define ICON_FA_CLOUD_RAIN "\xef\x9c\xbd" // U+f73d +#define ICON_FA_CLOUD_SHOWERS_HEAVY "\xef\x9d\x80" // U+f740 +#define ICON_FA_CLOUD_SUN "\xef\x9b\x84" // U+f6c4 +#define ICON_FA_CLOUD_SUN_RAIN "\xef\x9d\x83" // U+f743 +#define ICON_FA_CLOUD_UPLOAD_ALT "\xef\x8e\x82" // U+f382 +#define ICON_FA_COCKTAIL "\xef\x95\xa1" // U+f561 +#define ICON_FA_CODE "\xef\x84\xa1" // U+f121 +#define ICON_FA_CODE_BRANCH "\xef\x84\xa6" // U+f126 +#define ICON_FA_COFFEE "\xef\x83\xb4" // U+f0f4 +#define ICON_FA_COG "\xef\x80\x93" // U+f013 +#define ICON_FA_COGS "\xef\x82\x85" // U+f085 +#define ICON_FA_COINS "\xef\x94\x9e" // U+f51e +#define ICON_FA_COLUMNS "\xef\x83\x9b" // U+f0db +#define ICON_FA_COMMENT "\xef\x81\xb5" // U+f075 +#define ICON_FA_COMMENT_ALT "\xef\x89\xba" // U+f27a +#define ICON_FA_COMMENT_DOLLAR "\xef\x99\x91" // U+f651 +#define ICON_FA_COMMENT_DOTS "\xef\x92\xad" // U+f4ad +#define ICON_FA_COMMENT_MEDICAL "\xef\x9f\xb5" // U+f7f5 +#define ICON_FA_COMMENT_SLASH "\xef\x92\xb3" // U+f4b3 +#define ICON_FA_COMMENTS "\xef\x82\x86" // U+f086 +#define ICON_FA_COMMENTS_DOLLAR "\xef\x99\x93" // U+f653 +#define ICON_FA_COMPACT_DISC "\xef\x94\x9f" // U+f51f +#define ICON_FA_COMPASS "\xef\x85\x8e" // U+f14e +#define ICON_FA_COMPRESS "\xef\x81\xa6" // U+f066 +#define ICON_FA_COMPRESS_ALT "\xef\x90\xa2" // U+f422 +#define ICON_FA_COMPRESS_ARROWS_ALT "\xef\x9e\x8c" // U+f78c +#define ICON_FA_CONCIERGE_BELL "\xef\x95\xa2" // U+f562 +#define ICON_FA_COOKIE "\xef\x95\xa3" // U+f563 +#define ICON_FA_COOKIE_BITE "\xef\x95\xa4" // U+f564 +#define ICON_FA_COPY "\xef\x83\x85" // U+f0c5 +#define ICON_FA_COPYRIGHT "\xef\x87\xb9" // U+f1f9 +#define ICON_FA_COUCH "\xef\x92\xb8" // U+f4b8 +#define ICON_FA_CREDIT_CARD "\xef\x82\x9d" // U+f09d +#define ICON_FA_CROP "\xef\x84\xa5" // U+f125 +#define ICON_FA_CROP_ALT "\xef\x95\xa5" // U+f565 +#define ICON_FA_CROSS "\xef\x99\x94" // U+f654 +#define ICON_FA_CROSSHAIRS "\xef\x81\x9b" // U+f05b +#define ICON_FA_CROW "\xef\x94\xa0" // U+f520 +#define ICON_FA_CROWN "\xef\x94\xa1" // U+f521 +#define ICON_FA_CRUTCH "\xef\x9f\xb7" // U+f7f7 +#define ICON_FA_CUBE "\xef\x86\xb2" // U+f1b2 +#define ICON_FA_CUBES "\xef\x86\xb3" // U+f1b3 +#define ICON_FA_CUT "\xef\x83\x84" // U+f0c4 +#define ICON_FA_DATABASE "\xef\x87\x80" // U+f1c0 +#define ICON_FA_DEAF "\xef\x8a\xa4" // U+f2a4 +#define ICON_FA_DEMOCRAT "\xef\x9d\x87" // U+f747 +#define ICON_FA_DESKTOP "\xef\x84\x88" // U+f108 +#define ICON_FA_DHARMACHAKRA "\xef\x99\x95" // U+f655 +#define ICON_FA_DIAGNOSES "\xef\x91\xb0" // U+f470 +#define ICON_FA_DICE "\xef\x94\xa2" // U+f522 +#define ICON_FA_DICE_D20 "\xef\x9b\x8f" // U+f6cf +#define ICON_FA_DICE_D6 "\xef\x9b\x91" // U+f6d1 +#define ICON_FA_DICE_FIVE "\xef\x94\xa3" // U+f523 +#define ICON_FA_DICE_FOUR "\xef\x94\xa4" // U+f524 +#define ICON_FA_DICE_ONE "\xef\x94\xa5" // U+f525 +#define ICON_FA_DICE_SIX "\xef\x94\xa6" // U+f526 +#define ICON_FA_DICE_THREE "\xef\x94\xa7" // U+f527 +#define ICON_FA_DICE_TWO "\xef\x94\xa8" // U+f528 +#define ICON_FA_DIGITAL_TACHOGRAPH "\xef\x95\xa6" // U+f566 +#define ICON_FA_DIRECTIONS "\xef\x97\xab" // U+f5eb +#define ICON_FA_DISEASE "\xef\x9f\xba" // U+f7fa +#define ICON_FA_DIVIDE "\xef\x94\xa9" // U+f529 +#define ICON_FA_DIZZY "\xef\x95\xa7" // U+f567 +#define ICON_FA_DNA "\xef\x91\xb1" // U+f471 +#define ICON_FA_DOG "\xef\x9b\x93" // U+f6d3 +#define ICON_FA_DOLLAR_SIGN "\xef\x85\x95" // U+f155 +#define ICON_FA_DOLLY "\xef\x91\xb2" // U+f472 +#define ICON_FA_DOLLY_FLATBED "\xef\x91\xb4" // U+f474 +#define ICON_FA_DONATE "\xef\x92\xb9" // U+f4b9 +#define ICON_FA_DOOR_CLOSED "\xef\x94\xaa" // U+f52a +#define ICON_FA_DOOR_OPEN "\xef\x94\xab" // U+f52b +#define ICON_FA_DOT_CIRCLE "\xef\x86\x92" // U+f192 +#define ICON_FA_DOVE "\xef\x92\xba" // U+f4ba +#define ICON_FA_DOWNLOAD "\xef\x80\x99" // U+f019 +#define ICON_FA_DRAFTING_COMPASS "\xef\x95\xa8" // U+f568 +#define ICON_FA_DRAGON "\xef\x9b\x95" // U+f6d5 +#define ICON_FA_DRAW_POLYGON "\xef\x97\xae" // U+f5ee +#define ICON_FA_DRUM "\xef\x95\xa9" // U+f569 +#define ICON_FA_DRUM_STEELPAN "\xef\x95\xaa" // U+f56a +#define ICON_FA_DRUMSTICK_BITE "\xef\x9b\x97" // U+f6d7 +#define ICON_FA_DUMBBELL "\xef\x91\x8b" // U+f44b +#define ICON_FA_DUMPSTER "\xef\x9e\x93" // U+f793 +#define ICON_FA_DUMPSTER_FIRE "\xef\x9e\x94" // U+f794 +#define ICON_FA_DUNGEON "\xef\x9b\x99" // U+f6d9 +#define ICON_FA_EDIT "\xef\x81\x84" // U+f044 +#define ICON_FA_EGG "\xef\x9f\xbb" // U+f7fb +#define ICON_FA_EJECT "\xef\x81\x92" // U+f052 +#define ICON_FA_ELLIPSIS_H "\xef\x85\x81" // U+f141 +#define ICON_FA_ELLIPSIS_V "\xef\x85\x82" // U+f142 +#define ICON_FA_ENVELOPE "\xef\x83\xa0" // U+f0e0 +#define ICON_FA_ENVELOPE_OPEN "\xef\x8a\xb6" // U+f2b6 +#define ICON_FA_ENVELOPE_OPEN_TEXT "\xef\x99\x98" // U+f658 +#define ICON_FA_ENVELOPE_SQUARE "\xef\x86\x99" // U+f199 +#define ICON_FA_EQUALS "\xef\x94\xac" // U+f52c +#define ICON_FA_ERASER "\xef\x84\xad" // U+f12d +#define ICON_FA_ETHERNET "\xef\x9e\x96" // U+f796 +#define ICON_FA_EURO_SIGN "\xef\x85\x93" // U+f153 +#define ICON_FA_EXCHANGE_ALT "\xef\x8d\xa2" // U+f362 +#define ICON_FA_EXCLAMATION "\xef\x84\xaa" // U+f12a +#define ICON_FA_EXCLAMATION_CIRCLE "\xef\x81\xaa" // U+f06a +#define ICON_FA_EXCLAMATION_TRIANGLE "\xef\x81\xb1" // U+f071 +#define ICON_FA_EXPAND "\xef\x81\xa5" // U+f065 +#define ICON_FA_EXPAND_ALT "\xef\x90\xa4" // U+f424 +#define ICON_FA_EXPAND_ARROWS_ALT "\xef\x8c\x9e" // U+f31e +#define ICON_FA_EXTERNAL_LINK_ALT "\xef\x8d\x9d" // U+f35d +#define ICON_FA_EXTERNAL_LINK_SQUARE_ALT "\xef\x8d\xa0" // U+f360 +#define ICON_FA_EYE "\xef\x81\xae" // U+f06e +#define ICON_FA_EYE_DROPPER "\xef\x87\xbb" // U+f1fb +#define ICON_FA_EYE_SLASH "\xef\x81\xb0" // U+f070 +#define ICON_FA_FAN "\xef\xa1\xa3" // U+f863 +#define ICON_FA_FAST_BACKWARD "\xef\x81\x89" // U+f049 +#define ICON_FA_FAST_FORWARD "\xef\x81\x90" // U+f050 +#define ICON_FA_FAUCET "\xee\x80\x85" // U+e005 +#define ICON_FA_FAX "\xef\x86\xac" // U+f1ac +#define ICON_FA_FEATHER "\xef\x94\xad" // U+f52d +#define ICON_FA_FEATHER_ALT "\xef\x95\xab" // U+f56b +#define ICON_FA_FEMALE "\xef\x86\x82" // U+f182 +#define ICON_FA_FIGHTER_JET "\xef\x83\xbb" // U+f0fb +#define ICON_FA_FILE "\xef\x85\x9b" // U+f15b +#define ICON_FA_FILE_ALT "\xef\x85\x9c" // U+f15c +#define ICON_FA_FILE_ARCHIVE "\xef\x87\x86" // U+f1c6 +#define ICON_FA_FILE_AUDIO "\xef\x87\x87" // U+f1c7 +#define ICON_FA_FILE_CODE "\xef\x87\x89" // U+f1c9 +#define ICON_FA_FILE_CONTRACT "\xef\x95\xac" // U+f56c +#define ICON_FA_FILE_CSV "\xef\x9b\x9d" // U+f6dd +#define ICON_FA_FILE_DOWNLOAD "\xef\x95\xad" // U+f56d +#define ICON_FA_FILE_EXCEL "\xef\x87\x83" // U+f1c3 +#define ICON_FA_FILE_EXPORT "\xef\x95\xae" // U+f56e +#define ICON_FA_FILE_IMAGE "\xef\x87\x85" // U+f1c5 +#define ICON_FA_FILE_IMPORT "\xef\x95\xaf" // U+f56f +#define ICON_FA_FILE_INVOICE "\xef\x95\xb0" // U+f570 +#define ICON_FA_FILE_INVOICE_DOLLAR "\xef\x95\xb1" // U+f571 +#define ICON_FA_FILE_MEDICAL "\xef\x91\xb7" // U+f477 +#define ICON_FA_FILE_MEDICAL_ALT "\xef\x91\xb8" // U+f478 +#define ICON_FA_FILE_PDF "\xef\x87\x81" // U+f1c1 +#define ICON_FA_FILE_POWERPOINT "\xef\x87\x84" // U+f1c4 +#define ICON_FA_FILE_PRESCRIPTION "\xef\x95\xb2" // U+f572 +#define ICON_FA_FILE_SIGNATURE "\xef\x95\xb3" // U+f573 +#define ICON_FA_FILE_UPLOAD "\xef\x95\xb4" // U+f574 +#define ICON_FA_FILE_VIDEO "\xef\x87\x88" // U+f1c8 +#define ICON_FA_FILE_WORD "\xef\x87\x82" // U+f1c2 +#define ICON_FA_FILL "\xef\x95\xb5" // U+f575 +#define ICON_FA_FILL_DRIP "\xef\x95\xb6" // U+f576 +#define ICON_FA_FILM "\xef\x80\x88" // U+f008 +#define ICON_FA_FILTER "\xef\x82\xb0" // U+f0b0 +#define ICON_FA_FINGERPRINT "\xef\x95\xb7" // U+f577 +#define ICON_FA_FIRE "\xef\x81\xad" // U+f06d +#define ICON_FA_FIRE_ALT "\xef\x9f\xa4" // U+f7e4 +#define ICON_FA_FIRE_EXTINGUISHER "\xef\x84\xb4" // U+f134 +#define ICON_FA_FIRST_AID "\xef\x91\xb9" // U+f479 +#define ICON_FA_FISH "\xef\x95\xb8" // U+f578 +#define ICON_FA_FIST_RAISED "\xef\x9b\x9e" // U+f6de +#define ICON_FA_FLAG "\xef\x80\xa4" // U+f024 +#define ICON_FA_FLAG_CHECKERED "\xef\x84\x9e" // U+f11e +#define ICON_FA_FLAG_USA "\xef\x9d\x8d" // U+f74d +#define ICON_FA_FLASK "\xef\x83\x83" // U+f0c3 +#define ICON_FA_FLUSHED "\xef\x95\xb9" // U+f579 +#define ICON_FA_FOLDER "\xef\x81\xbb" // U+f07b +#define ICON_FA_FOLDER_MINUS "\xef\x99\x9d" // U+f65d +#define ICON_FA_FOLDER_OPEN "\xef\x81\xbc" // U+f07c +#define ICON_FA_FOLDER_PLUS "\xef\x99\x9e" // U+f65e +#define ICON_FA_FONT "\xef\x80\xb1" // U+f031 +#define ICON_FA_FONT_AWESOME_LOGO_FULL "\xef\x93\xa6" // U+f4e6 +#define ICON_FA_FOOTBALL_BALL "\xef\x91\x8e" // U+f44e +#define ICON_FA_FORWARD "\xef\x81\x8e" // U+f04e +#define ICON_FA_FROG "\xef\x94\xae" // U+f52e +#define ICON_FA_FROWN "\xef\x84\x99" // U+f119 +#define ICON_FA_FROWN_OPEN "\xef\x95\xba" // U+f57a +#define ICON_FA_FUNNEL_DOLLAR "\xef\x99\xa2" // U+f662 +#define ICON_FA_FUTBOL "\xef\x87\xa3" // U+f1e3 +#define ICON_FA_GAMEPAD "\xef\x84\x9b" // U+f11b +#define ICON_FA_GAS_PUMP "\xef\x94\xaf" // U+f52f +#define ICON_FA_GAVEL "\xef\x83\xa3" // U+f0e3 +#define ICON_FA_GEM "\xef\x8e\xa5" // U+f3a5 +#define ICON_FA_GENDERLESS "\xef\x88\xad" // U+f22d +#define ICON_FA_GHOST "\xef\x9b\xa2" // U+f6e2 +#define ICON_FA_GIFT "\xef\x81\xab" // U+f06b +#define ICON_FA_GIFTS "\xef\x9e\x9c" // U+f79c +#define ICON_FA_GLASS_CHEERS "\xef\x9e\x9f" // U+f79f +#define ICON_FA_GLASS_MARTINI "\xef\x80\x80" // U+f000 +#define ICON_FA_GLASS_MARTINI_ALT "\xef\x95\xbb" // U+f57b +#define ICON_FA_GLASS_WHISKEY "\xef\x9e\xa0" // U+f7a0 +#define ICON_FA_GLASSES "\xef\x94\xb0" // U+f530 +#define ICON_FA_GLOBE "\xef\x82\xac" // U+f0ac +#define ICON_FA_GLOBE_AFRICA "\xef\x95\xbc" // U+f57c +#define ICON_FA_GLOBE_AMERICAS "\xef\x95\xbd" // U+f57d +#define ICON_FA_GLOBE_ASIA "\xef\x95\xbe" // U+f57e +#define ICON_FA_GLOBE_EUROPE "\xef\x9e\xa2" // U+f7a2 +#define ICON_FA_GOLF_BALL "\xef\x91\x90" // U+f450 +#define ICON_FA_GOPURAM "\xef\x99\xa4" // U+f664 +#define ICON_FA_GRADUATION_CAP "\xef\x86\x9d" // U+f19d +#define ICON_FA_GREATER_THAN "\xef\x94\xb1" // U+f531 +#define ICON_FA_GREATER_THAN_EQUAL "\xef\x94\xb2" // U+f532 +#define ICON_FA_GRIMACE "\xef\x95\xbf" // U+f57f +#define ICON_FA_GRIN "\xef\x96\x80" // U+f580 +#define ICON_FA_GRIN_ALT "\xef\x96\x81" // U+f581 +#define ICON_FA_GRIN_BEAM "\xef\x96\x82" // U+f582 +#define ICON_FA_GRIN_BEAM_SWEAT "\xef\x96\x83" // U+f583 +#define ICON_FA_GRIN_HEARTS "\xef\x96\x84" // U+f584 +#define ICON_FA_GRIN_SQUINT "\xef\x96\x85" // U+f585 +#define ICON_FA_GRIN_SQUINT_TEARS "\xef\x96\x86" // U+f586 +#define ICON_FA_GRIN_STARS "\xef\x96\x87" // U+f587 +#define ICON_FA_GRIN_TEARS "\xef\x96\x88" // U+f588 +#define ICON_FA_GRIN_TONGUE "\xef\x96\x89" // U+f589 +#define ICON_FA_GRIN_TONGUE_SQUINT "\xef\x96\x8a" // U+f58a +#define ICON_FA_GRIN_TONGUE_WINK "\xef\x96\x8b" // U+f58b +#define ICON_FA_GRIN_WINK "\xef\x96\x8c" // U+f58c +#define ICON_FA_GRIP_HORIZONTAL "\xef\x96\x8d" // U+f58d +#define ICON_FA_GRIP_LINES "\xef\x9e\xa4" // U+f7a4 +#define ICON_FA_GRIP_LINES_VERTICAL "\xef\x9e\xa5" // U+f7a5 +#define ICON_FA_GRIP_VERTICAL "\xef\x96\x8e" // U+f58e +#define ICON_FA_GUITAR "\xef\x9e\xa6" // U+f7a6 +#define ICON_FA_H_SQUARE "\xef\x83\xbd" // U+f0fd +#define ICON_FA_HAMBURGER "\xef\xa0\x85" // U+f805 +#define ICON_FA_HAMMER "\xef\x9b\xa3" // U+f6e3 +#define ICON_FA_HAMSA "\xef\x99\xa5" // U+f665 +#define ICON_FA_HAND_HOLDING "\xef\x92\xbd" // U+f4bd +#define ICON_FA_HAND_HOLDING_HEART "\xef\x92\xbe" // U+f4be +#define ICON_FA_HAND_HOLDING_MEDICAL "\xee\x81\x9c" // U+e05c +#define ICON_FA_HAND_HOLDING_USD "\xef\x93\x80" // U+f4c0 +#define ICON_FA_HAND_HOLDING_WATER "\xef\x93\x81" // U+f4c1 +#define ICON_FA_HAND_LIZARD "\xef\x89\x98" // U+f258 +#define ICON_FA_HAND_MIDDLE_FINGER "\xef\xa0\x86" // U+f806 +#define ICON_FA_HAND_PAPER "\xef\x89\x96" // U+f256 +#define ICON_FA_HAND_PEACE "\xef\x89\x9b" // U+f25b +#define ICON_FA_HAND_POINT_DOWN "\xef\x82\xa7" // U+f0a7 +#define ICON_FA_HAND_POINT_LEFT "\xef\x82\xa5" // U+f0a5 +#define ICON_FA_HAND_POINT_RIGHT "\xef\x82\xa4" // U+f0a4 +#define ICON_FA_HAND_POINT_UP "\xef\x82\xa6" // U+f0a6 +#define ICON_FA_HAND_POINTER "\xef\x89\x9a" // U+f25a +#define ICON_FA_HAND_ROCK "\xef\x89\x95" // U+f255 +#define ICON_FA_HAND_SCISSORS "\xef\x89\x97" // U+f257 +#define ICON_FA_HAND_SPARKLES "\xee\x81\x9d" // U+e05d +#define ICON_FA_HAND_SPOCK "\xef\x89\x99" // U+f259 +#define ICON_FA_HANDS "\xef\x93\x82" // U+f4c2 +#define ICON_FA_HANDS_HELPING "\xef\x93\x84" // U+f4c4 +#define ICON_FA_HANDS_WASH "\xee\x81\x9e" // U+e05e +#define ICON_FA_HANDSHAKE "\xef\x8a\xb5" // U+f2b5 +#define ICON_FA_HANDSHAKE_ALT_SLASH "\xee\x81\x9f" // U+e05f +#define ICON_FA_HANDSHAKE_SLASH "\xee\x81\xa0" // U+e060 +#define ICON_FA_HANUKIAH "\xef\x9b\xa6" // U+f6e6 +#define ICON_FA_HARD_HAT "\xef\xa0\x87" // U+f807 +#define ICON_FA_HASHTAG "\xef\x8a\x92" // U+f292 +#define ICON_FA_HAT_COWBOY "\xef\xa3\x80" // U+f8c0 +#define ICON_FA_HAT_COWBOY_SIDE "\xef\xa3\x81" // U+f8c1 +#define ICON_FA_HAT_WIZARD "\xef\x9b\xa8" // U+f6e8 +#define ICON_FA_HDD "\xef\x82\xa0" // U+f0a0 +#define ICON_FA_HEAD_SIDE_COUGH "\xee\x81\xa1" // U+e061 +#define ICON_FA_HEAD_SIDE_COUGH_SLASH "\xee\x81\xa2" // U+e062 +#define ICON_FA_HEAD_SIDE_MASK "\xee\x81\xa3" // U+e063 +#define ICON_FA_HEAD_SIDE_VIRUS "\xee\x81\xa4" // U+e064 +#define ICON_FA_HEADING "\xef\x87\x9c" // U+f1dc +#define ICON_FA_HEADPHONES "\xef\x80\xa5" // U+f025 +#define ICON_FA_HEADPHONES_ALT "\xef\x96\x8f" // U+f58f +#define ICON_FA_HEADSET "\xef\x96\x90" // U+f590 +#define ICON_FA_HEART "\xef\x80\x84" // U+f004 +#define ICON_FA_HEART_BROKEN "\xef\x9e\xa9" // U+f7a9 +#define ICON_FA_HEARTBEAT "\xef\x88\x9e" // U+f21e +#define ICON_FA_HELICOPTER "\xef\x94\xb3" // U+f533 +#define ICON_FA_HIGHLIGHTER "\xef\x96\x91" // U+f591 +#define ICON_FA_HIKING "\xef\x9b\xac" // U+f6ec +#define ICON_FA_HIPPO "\xef\x9b\xad" // U+f6ed +#define ICON_FA_HISTORY "\xef\x87\x9a" // U+f1da +#define ICON_FA_HOCKEY_PUCK "\xef\x91\x93" // U+f453 +#define ICON_FA_HOLLY_BERRY "\xef\x9e\xaa" // U+f7aa +#define ICON_FA_HOME "\xef\x80\x95" // U+f015 +#define ICON_FA_HORSE "\xef\x9b\xb0" // U+f6f0 +#define ICON_FA_HORSE_HEAD "\xef\x9e\xab" // U+f7ab +#define ICON_FA_HOSPITAL "\xef\x83\xb8" // U+f0f8 +#define ICON_FA_HOSPITAL_ALT "\xef\x91\xbd" // U+f47d +#define ICON_FA_HOSPITAL_SYMBOL "\xef\x91\xbe" // U+f47e +#define ICON_FA_HOSPITAL_USER "\xef\xa0\x8d" // U+f80d +#define ICON_FA_HOT_TUB "\xef\x96\x93" // U+f593 +#define ICON_FA_HOTDOG "\xef\xa0\x8f" // U+f80f +#define ICON_FA_HOTEL "\xef\x96\x94" // U+f594 +#define ICON_FA_HOURGLASS "\xef\x89\x94" // U+f254 +#define ICON_FA_HOURGLASS_END "\xef\x89\x93" // U+f253 +#define ICON_FA_HOURGLASS_HALF "\xef\x89\x92" // U+f252 +#define ICON_FA_HOURGLASS_START "\xef\x89\x91" // U+f251 +#define ICON_FA_HOUSE_DAMAGE "\xef\x9b\xb1" // U+f6f1 +#define ICON_FA_HOUSE_USER "\xee\x81\xa5" // U+e065 +#define ICON_FA_HRYVNIA "\xef\x9b\xb2" // U+f6f2 +#define ICON_FA_I_CURSOR "\xef\x89\x86" // U+f246 +#define ICON_FA_ICE_CREAM "\xef\xa0\x90" // U+f810 +#define ICON_FA_ICICLES "\xef\x9e\xad" // U+f7ad +#define ICON_FA_ICONS "\xef\xa1\xad" // U+f86d +#define ICON_FA_ID_BADGE "\xef\x8b\x81" // U+f2c1 +#define ICON_FA_ID_CARD "\xef\x8b\x82" // U+f2c2 +#define ICON_FA_ID_CARD_ALT "\xef\x91\xbf" // U+f47f +#define ICON_FA_IGLOO "\xef\x9e\xae" // U+f7ae +#define ICON_FA_IMAGE "\xef\x80\xbe" // U+f03e +#define ICON_FA_IMAGES "\xef\x8c\x82" // U+f302 +#define ICON_FA_INBOX "\xef\x80\x9c" // U+f01c +#define ICON_FA_INDENT "\xef\x80\xbc" // U+f03c +#define ICON_FA_INDUSTRY "\xef\x89\xb5" // U+f275 +#define ICON_FA_INFINITY "\xef\x94\xb4" // U+f534 +#define ICON_FA_INFO "\xef\x84\xa9" // U+f129 +#define ICON_FA_INFO_CIRCLE "\xef\x81\x9a" // U+f05a +#define ICON_FA_ITALIC "\xef\x80\xb3" // U+f033 +#define ICON_FA_JEDI "\xef\x99\xa9" // U+f669 +#define ICON_FA_JOINT "\xef\x96\x95" // U+f595 +#define ICON_FA_JOURNAL_WHILLS "\xef\x99\xaa" // U+f66a +#define ICON_FA_KAABA "\xef\x99\xab" // U+f66b +#define ICON_FA_KEY "\xef\x82\x84" // U+f084 +#define ICON_FA_KEYBOARD "\xef\x84\x9c" // U+f11c +#define ICON_FA_KHANDA "\xef\x99\xad" // U+f66d +#define ICON_FA_KISS "\xef\x96\x96" // U+f596 +#define ICON_FA_KISS_BEAM "\xef\x96\x97" // U+f597 +#define ICON_FA_KISS_WINK_HEART "\xef\x96\x98" // U+f598 +#define ICON_FA_KIWI_BIRD "\xef\x94\xb5" // U+f535 +#define ICON_FA_LANDMARK "\xef\x99\xaf" // U+f66f +#define ICON_FA_LANGUAGE "\xef\x86\xab" // U+f1ab +#define ICON_FA_LAPTOP "\xef\x84\x89" // U+f109 +#define ICON_FA_LAPTOP_CODE "\xef\x97\xbc" // U+f5fc +#define ICON_FA_LAPTOP_HOUSE "\xee\x81\xa6" // U+e066 +#define ICON_FA_LAPTOP_MEDICAL "\xef\xa0\x92" // U+f812 +#define ICON_FA_LAUGH "\xef\x96\x99" // U+f599 +#define ICON_FA_LAUGH_BEAM "\xef\x96\x9a" // U+f59a +#define ICON_FA_LAUGH_SQUINT "\xef\x96\x9b" // U+f59b +#define ICON_FA_LAUGH_WINK "\xef\x96\x9c" // U+f59c +#define ICON_FA_LAYER_GROUP "\xef\x97\xbd" // U+f5fd +#define ICON_FA_LEAF "\xef\x81\xac" // U+f06c +#define ICON_FA_LEMON "\xef\x82\x94" // U+f094 +#define ICON_FA_LESS_THAN "\xef\x94\xb6" // U+f536 +#define ICON_FA_LESS_THAN_EQUAL "\xef\x94\xb7" // U+f537 +#define ICON_FA_LEVEL_DOWN_ALT "\xef\x8e\xbe" // U+f3be +#define ICON_FA_LEVEL_UP_ALT "\xef\x8e\xbf" // U+f3bf +#define ICON_FA_LIFE_RING "\xef\x87\x8d" // U+f1cd +#define ICON_FA_LIGHTBULB "\xef\x83\xab" // U+f0eb +#define ICON_FA_LINK "\xef\x83\x81" // U+f0c1 +#define ICON_FA_LIRA_SIGN "\xef\x86\x95" // U+f195 +#define ICON_FA_LIST "\xef\x80\xba" // U+f03a +#define ICON_FA_LIST_ALT "\xef\x80\xa2" // U+f022 +#define ICON_FA_LIST_OL "\xef\x83\x8b" // U+f0cb +#define ICON_FA_LIST_UL "\xef\x83\x8a" // U+f0ca +#define ICON_FA_LOCATION_ARROW "\xef\x84\xa4" // U+f124 +#define ICON_FA_LOCK "\xef\x80\xa3" // U+f023 +#define ICON_FA_LOCK_OPEN "\xef\x8f\x81" // U+f3c1 +#define ICON_FA_LONG_ARROW_ALT_DOWN "\xef\x8c\x89" // U+f309 +#define ICON_FA_LONG_ARROW_ALT_LEFT "\xef\x8c\x8a" // U+f30a +#define ICON_FA_LONG_ARROW_ALT_RIGHT "\xef\x8c\x8b" // U+f30b +#define ICON_FA_LONG_ARROW_ALT_UP "\xef\x8c\x8c" // U+f30c +#define ICON_FA_LOW_VISION "\xef\x8a\xa8" // U+f2a8 +#define ICON_FA_LUGGAGE_CART "\xef\x96\x9d" // U+f59d +#define ICON_FA_LUNGS "\xef\x98\x84" // U+f604 +#define ICON_FA_LUNGS_VIRUS "\xee\x81\xa7" // U+e067 +#define ICON_FA_MAGIC "\xef\x83\x90" // U+f0d0 +#define ICON_FA_MAGNET "\xef\x81\xb6" // U+f076 +#define ICON_FA_MAIL_BULK "\xef\x99\xb4" // U+f674 +#define ICON_FA_MALE "\xef\x86\x83" // U+f183 +#define ICON_FA_MAP "\xef\x89\xb9" // U+f279 +#define ICON_FA_MAP_MARKED "\xef\x96\x9f" // U+f59f +#define ICON_FA_MAP_MARKED_ALT "\xef\x96\xa0" // U+f5a0 +#define ICON_FA_MAP_MARKER "\xef\x81\x81" // U+f041 +#define ICON_FA_MAP_MARKER_ALT "\xef\x8f\x85" // U+f3c5 +#define ICON_FA_MAP_PIN "\xef\x89\xb6" // U+f276 +#define ICON_FA_MAP_SIGNS "\xef\x89\xb7" // U+f277 +#define ICON_FA_MARKER "\xef\x96\xa1" // U+f5a1 +#define ICON_FA_MARS "\xef\x88\xa2" // U+f222 +#define ICON_FA_MARS_DOUBLE "\xef\x88\xa7" // U+f227 +#define ICON_FA_MARS_STROKE "\xef\x88\xa9" // U+f229 +#define ICON_FA_MARS_STROKE_H "\xef\x88\xab" // U+f22b +#define ICON_FA_MARS_STROKE_V "\xef\x88\xaa" // U+f22a +#define ICON_FA_MASK "\xef\x9b\xba" // U+f6fa +#define ICON_FA_MEDAL "\xef\x96\xa2" // U+f5a2 +#define ICON_FA_MEDKIT "\xef\x83\xba" // U+f0fa +#define ICON_FA_MEH "\xef\x84\x9a" // U+f11a +#define ICON_FA_MEH_BLANK "\xef\x96\xa4" // U+f5a4 +#define ICON_FA_MEH_ROLLING_EYES "\xef\x96\xa5" // U+f5a5 +#define ICON_FA_MEMORY "\xef\x94\xb8" // U+f538 +#define ICON_FA_MENORAH "\xef\x99\xb6" // U+f676 +#define ICON_FA_MERCURY "\xef\x88\xa3" // U+f223 +#define ICON_FA_METEOR "\xef\x9d\x93" // U+f753 +#define ICON_FA_MICROCHIP "\xef\x8b\x9b" // U+f2db +#define ICON_FA_MICROPHONE "\xef\x84\xb0" // U+f130 +#define ICON_FA_MICROPHONE_ALT "\xef\x8f\x89" // U+f3c9 +#define ICON_FA_MICROPHONE_ALT_SLASH "\xef\x94\xb9" // U+f539 +#define ICON_FA_MICROPHONE_SLASH "\xef\x84\xb1" // U+f131 +#define ICON_FA_MICROSCOPE "\xef\x98\x90" // U+f610 +#define ICON_FA_MINUS "\xef\x81\xa8" // U+f068 +#define ICON_FA_MINUS_CIRCLE "\xef\x81\x96" // U+f056 +#define ICON_FA_MINUS_SQUARE "\xef\x85\x86" // U+f146 +#define ICON_FA_MITTEN "\xef\x9e\xb5" // U+f7b5 +#define ICON_FA_MOBILE "\xef\x84\x8b" // U+f10b +#define ICON_FA_MOBILE_ALT "\xef\x8f\x8d" // U+f3cd +#define ICON_FA_MONEY_BILL "\xef\x83\x96" // U+f0d6 +#define ICON_FA_MONEY_BILL_ALT "\xef\x8f\x91" // U+f3d1 +#define ICON_FA_MONEY_BILL_WAVE "\xef\x94\xba" // U+f53a +#define ICON_FA_MONEY_BILL_WAVE_ALT "\xef\x94\xbb" // U+f53b +#define ICON_FA_MONEY_CHECK "\xef\x94\xbc" // U+f53c +#define ICON_FA_MONEY_CHECK_ALT "\xef\x94\xbd" // U+f53d +#define ICON_FA_MONUMENT "\xef\x96\xa6" // U+f5a6 +#define ICON_FA_MOON "\xef\x86\x86" // U+f186 +#define ICON_FA_MORTAR_PESTLE "\xef\x96\xa7" // U+f5a7 +#define ICON_FA_MOSQUE "\xef\x99\xb8" // U+f678 +#define ICON_FA_MOTORCYCLE "\xef\x88\x9c" // U+f21c +#define ICON_FA_MOUNTAIN "\xef\x9b\xbc" // U+f6fc +#define ICON_FA_MOUSE "\xef\xa3\x8c" // U+f8cc +#define ICON_FA_MOUSE_POINTER "\xef\x89\x85" // U+f245 +#define ICON_FA_MUG_HOT "\xef\x9e\xb6" // U+f7b6 +#define ICON_FA_MUSIC "\xef\x80\x81" // U+f001 +#define ICON_FA_NETWORK_WIRED "\xef\x9b\xbf" // U+f6ff +#define ICON_FA_NEUTER "\xef\x88\xac" // U+f22c +#define ICON_FA_NEWSPAPER "\xef\x87\xaa" // U+f1ea +#define ICON_FA_NOT_EQUAL "\xef\x94\xbe" // U+f53e +#define ICON_FA_NOTES_MEDICAL "\xef\x92\x81" // U+f481 +#define ICON_FA_OBJECT_GROUP "\xef\x89\x87" // U+f247 +#define ICON_FA_OBJECT_UNGROUP "\xef\x89\x88" // U+f248 +#define ICON_FA_OIL_CAN "\xef\x98\x93" // U+f613 +#define ICON_FA_OM "\xef\x99\xb9" // U+f679 +#define ICON_FA_OTTER "\xef\x9c\x80" // U+f700 +#define ICON_FA_OUTDENT "\xef\x80\xbb" // U+f03b +#define ICON_FA_PAGER "\xef\xa0\x95" // U+f815 +#define ICON_FA_PAINT_BRUSH "\xef\x87\xbc" // U+f1fc +#define ICON_FA_PAINT_ROLLER "\xef\x96\xaa" // U+f5aa +#define ICON_FA_PALETTE "\xef\x94\xbf" // U+f53f +#define ICON_FA_PALLET "\xef\x92\x82" // U+f482 +#define ICON_FA_PAPER_PLANE "\xef\x87\x98" // U+f1d8 +#define ICON_FA_PAPERCLIP "\xef\x83\x86" // U+f0c6 +#define ICON_FA_PARACHUTE_BOX "\xef\x93\x8d" // U+f4cd +#define ICON_FA_PARAGRAPH "\xef\x87\x9d" // U+f1dd +#define ICON_FA_PARKING "\xef\x95\x80" // U+f540 +#define ICON_FA_PASSPORT "\xef\x96\xab" // U+f5ab +#define ICON_FA_PASTAFARIANISM "\xef\x99\xbb" // U+f67b +#define ICON_FA_PASTE "\xef\x83\xaa" // U+f0ea +#define ICON_FA_PAUSE "\xef\x81\x8c" // U+f04c +#define ICON_FA_PAUSE_CIRCLE "\xef\x8a\x8b" // U+f28b +#define ICON_FA_PAW "\xef\x86\xb0" // U+f1b0 +#define ICON_FA_PEACE "\xef\x99\xbc" // U+f67c +#define ICON_FA_PEN "\xef\x8c\x84" // U+f304 +#define ICON_FA_PEN_ALT "\xef\x8c\x85" // U+f305 +#define ICON_FA_PEN_FANCY "\xef\x96\xac" // U+f5ac +#define ICON_FA_PEN_NIB "\xef\x96\xad" // U+f5ad +#define ICON_FA_PEN_SQUARE "\xef\x85\x8b" // U+f14b +#define ICON_FA_PENCIL_ALT "\xef\x8c\x83" // U+f303 +#define ICON_FA_PENCIL_RULER "\xef\x96\xae" // U+f5ae +#define ICON_FA_PEOPLE_ARROWS "\xee\x81\xa8" // U+e068 +#define ICON_FA_PEOPLE_CARRY "\xef\x93\x8e" // U+f4ce +#define ICON_FA_PEPPER_HOT "\xef\xa0\x96" // U+f816 +#define ICON_FA_PERCENT "\xef\x8a\x95" // U+f295 +#define ICON_FA_PERCENTAGE "\xef\x95\x81" // U+f541 +#define ICON_FA_PERSON_BOOTH "\xef\x9d\x96" // U+f756 +#define ICON_FA_PHONE "\xef\x82\x95" // U+f095 +#define ICON_FA_PHONE_ALT "\xef\xa1\xb9" // U+f879 +#define ICON_FA_PHONE_SLASH "\xef\x8f\x9d" // U+f3dd +#define ICON_FA_PHONE_SQUARE "\xef\x82\x98" // U+f098 +#define ICON_FA_PHONE_SQUARE_ALT "\xef\xa1\xbb" // U+f87b +#define ICON_FA_PHONE_VOLUME "\xef\x8a\xa0" // U+f2a0 +#define ICON_FA_PHOTO_VIDEO "\xef\xa1\xbc" // U+f87c +#define ICON_FA_PIGGY_BANK "\xef\x93\x93" // U+f4d3 +#define ICON_FA_PILLS "\xef\x92\x84" // U+f484 +#define ICON_FA_PIZZA_SLICE "\xef\xa0\x98" // U+f818 +#define ICON_FA_PLACE_OF_WORSHIP "\xef\x99\xbf" // U+f67f +#define ICON_FA_PLANE "\xef\x81\xb2" // U+f072 +#define ICON_FA_PLANE_ARRIVAL "\xef\x96\xaf" // U+f5af +#define ICON_FA_PLANE_DEPARTURE "\xef\x96\xb0" // U+f5b0 +#define ICON_FA_PLANE_SLASH "\xee\x81\xa9" // U+e069 +#define ICON_FA_PLAY "\xef\x81\x8b" // U+f04b +#define ICON_FA_PLAY_CIRCLE "\xef\x85\x84" // U+f144 +#define ICON_FA_PLUG "\xef\x87\xa6" // U+f1e6 +#define ICON_FA_PLUS "\xef\x81\xa7" // U+f067 +#define ICON_FA_PLUS_CIRCLE "\xef\x81\x95" // U+f055 +#define ICON_FA_PLUS_SQUARE "\xef\x83\xbe" // U+f0fe +#define ICON_FA_PODCAST "\xef\x8b\x8e" // U+f2ce +#define ICON_FA_POLL "\xef\x9a\x81" // U+f681 +#define ICON_FA_POLL_H "\xef\x9a\x82" // U+f682 +#define ICON_FA_POO "\xef\x8b\xbe" // U+f2fe +#define ICON_FA_POO_STORM "\xef\x9d\x9a" // U+f75a +#define ICON_FA_POOP "\xef\x98\x99" // U+f619 +#define ICON_FA_PORTRAIT "\xef\x8f\xa0" // U+f3e0 +#define ICON_FA_POUND_SIGN "\xef\x85\x94" // U+f154 +#define ICON_FA_POWER_OFF "\xef\x80\x91" // U+f011 +#define ICON_FA_PRAY "\xef\x9a\x83" // U+f683 +#define ICON_FA_PRAYING_HANDS "\xef\x9a\x84" // U+f684 +#define ICON_FA_PRESCRIPTION "\xef\x96\xb1" // U+f5b1 +#define ICON_FA_PRESCRIPTION_BOTTLE "\xef\x92\x85" // U+f485 +#define ICON_FA_PRESCRIPTION_BOTTLE_ALT "\xef\x92\x86" // U+f486 +#define ICON_FA_PRINT "\xef\x80\xaf" // U+f02f +#define ICON_FA_PROCEDURES "\xef\x92\x87" // U+f487 +#define ICON_FA_PROJECT_DIAGRAM "\xef\x95\x82" // U+f542 +#define ICON_FA_PUMP_MEDICAL "\xee\x81\xaa" // U+e06a +#define ICON_FA_PUMP_SOAP "\xee\x81\xab" // U+e06b +#define ICON_FA_PUZZLE_PIECE "\xef\x84\xae" // U+f12e +#define ICON_FA_QRCODE "\xef\x80\xa9" // U+f029 +#define ICON_FA_QUESTION "\xef\x84\xa8" // U+f128 +#define ICON_FA_QUESTION_CIRCLE "\xef\x81\x99" // U+f059 +#define ICON_FA_QUIDDITCH "\xef\x91\x98" // U+f458 +#define ICON_FA_QUOTE_LEFT "\xef\x84\x8d" // U+f10d +#define ICON_FA_QUOTE_RIGHT "\xef\x84\x8e" // U+f10e +#define ICON_FA_QURAN "\xef\x9a\x87" // U+f687 +#define ICON_FA_RADIATION "\xef\x9e\xb9" // U+f7b9 +#define ICON_FA_RADIATION_ALT "\xef\x9e\xba" // U+f7ba +#define ICON_FA_RAINBOW "\xef\x9d\x9b" // U+f75b +#define ICON_FA_RANDOM "\xef\x81\xb4" // U+f074 +#define ICON_FA_RECEIPT "\xef\x95\x83" // U+f543 +#define ICON_FA_RECORD_VINYL "\xef\xa3\x99" // U+f8d9 +#define ICON_FA_RECYCLE "\xef\x86\xb8" // U+f1b8 +#define ICON_FA_REDO "\xef\x80\x9e" // U+f01e +#define ICON_FA_REDO_ALT "\xef\x8b\xb9" // U+f2f9 +#define ICON_FA_REGISTERED "\xef\x89\x9d" // U+f25d +#define ICON_FA_REMOVE_FORMAT "\xef\xa1\xbd" // U+f87d +#define ICON_FA_REPLY "\xef\x8f\xa5" // U+f3e5 +#define ICON_FA_REPLY_ALL "\xef\x84\xa2" // U+f122 +#define ICON_FA_REPUBLICAN "\xef\x9d\x9e" // U+f75e +#define ICON_FA_RESTROOM "\xef\x9e\xbd" // U+f7bd +#define ICON_FA_RETWEET "\xef\x81\xb9" // U+f079 +#define ICON_FA_RIBBON "\xef\x93\x96" // U+f4d6 +#define ICON_FA_RING "\xef\x9c\x8b" // U+f70b +#define ICON_FA_ROAD "\xef\x80\x98" // U+f018 +#define ICON_FA_ROBOT "\xef\x95\x84" // U+f544 +#define ICON_FA_ROCKET "\xef\x84\xb5" // U+f135 +#define ICON_FA_ROUTE "\xef\x93\x97" // U+f4d7 +#define ICON_FA_RSS "\xef\x82\x9e" // U+f09e +#define ICON_FA_RSS_SQUARE "\xef\x85\x83" // U+f143 +#define ICON_FA_RUBLE_SIGN "\xef\x85\x98" // U+f158 +#define ICON_FA_RULER "\xef\x95\x85" // U+f545 +#define ICON_FA_RULER_COMBINED "\xef\x95\x86" // U+f546 +#define ICON_FA_RULER_HORIZONTAL "\xef\x95\x87" // U+f547 +#define ICON_FA_RULER_VERTICAL "\xef\x95\x88" // U+f548 +#define ICON_FA_RUNNING "\xef\x9c\x8c" // U+f70c +#define ICON_FA_RUPEE_SIGN "\xef\x85\x96" // U+f156 +#define ICON_FA_SAD_CRY "\xef\x96\xb3" // U+f5b3 +#define ICON_FA_SAD_TEAR "\xef\x96\xb4" // U+f5b4 +#define ICON_FA_SATELLITE "\xef\x9e\xbf" // U+f7bf +#define ICON_FA_SATELLITE_DISH "\xef\x9f\x80" // U+f7c0 +#define ICON_FA_SAVE "\xef\x83\x87" // U+f0c7 +#define ICON_FA_SCHOOL "\xef\x95\x89" // U+f549 +#define ICON_FA_SCREWDRIVER "\xef\x95\x8a" // U+f54a +#define ICON_FA_SCROLL "\xef\x9c\x8e" // U+f70e +#define ICON_FA_SD_CARD "\xef\x9f\x82" // U+f7c2 +#define ICON_FA_SEARCH "\xef\x80\x82" // U+f002 +#define ICON_FA_SEARCH_DOLLAR "\xef\x9a\x88" // U+f688 +#define ICON_FA_SEARCH_LOCATION "\xef\x9a\x89" // U+f689 +#define ICON_FA_SEARCH_MINUS "\xef\x80\x90" // U+f010 +#define ICON_FA_SEARCH_PLUS "\xef\x80\x8e" // U+f00e +#define ICON_FA_SEEDLING "\xef\x93\x98" // U+f4d8 +#define ICON_FA_SERVER "\xef\x88\xb3" // U+f233 +#define ICON_FA_SHAPES "\xef\x98\x9f" // U+f61f +#define ICON_FA_SHARE "\xef\x81\xa4" // U+f064 +#define ICON_FA_SHARE_ALT "\xef\x87\xa0" // U+f1e0 +#define ICON_FA_SHARE_ALT_SQUARE "\xef\x87\xa1" // U+f1e1 +#define ICON_FA_SHARE_SQUARE "\xef\x85\x8d" // U+f14d +#define ICON_FA_SHEKEL_SIGN "\xef\x88\x8b" // U+f20b +#define ICON_FA_SHIELD_ALT "\xef\x8f\xad" // U+f3ed +#define ICON_FA_SHIELD_VIRUS "\xee\x81\xac" // U+e06c +#define ICON_FA_SHIP "\xef\x88\x9a" // U+f21a +#define ICON_FA_SHIPPING_FAST "\xef\x92\x8b" // U+f48b +#define ICON_FA_SHOE_PRINTS "\xef\x95\x8b" // U+f54b +#define ICON_FA_SHOPPING_BAG "\xef\x8a\x90" // U+f290 +#define ICON_FA_SHOPPING_BASKET "\xef\x8a\x91" // U+f291 +#define ICON_FA_SHOPPING_CART "\xef\x81\xba" // U+f07a +#define ICON_FA_SHOWER "\xef\x8b\x8c" // U+f2cc +#define ICON_FA_SHUTTLE_VAN "\xef\x96\xb6" // U+f5b6 +#define ICON_FA_SIGN "\xef\x93\x99" // U+f4d9 +#define ICON_FA_SIGN_IN_ALT "\xef\x8b\xb6" // U+f2f6 +#define ICON_FA_SIGN_LANGUAGE "\xef\x8a\xa7" // U+f2a7 +#define ICON_FA_SIGN_OUT_ALT "\xef\x8b\xb5" // U+f2f5 +#define ICON_FA_SIGNAL "\xef\x80\x92" // U+f012 +#define ICON_FA_SIGNATURE "\xef\x96\xb7" // U+f5b7 +#define ICON_FA_SIM_CARD "\xef\x9f\x84" // U+f7c4 +#define ICON_FA_SINK "\xee\x81\xad" // U+e06d +#define ICON_FA_SITEMAP "\xef\x83\xa8" // U+f0e8 +#define ICON_FA_SKATING "\xef\x9f\x85" // U+f7c5 +#define ICON_FA_SKIING "\xef\x9f\x89" // U+f7c9 +#define ICON_FA_SKIING_NORDIC "\xef\x9f\x8a" // U+f7ca +#define ICON_FA_SKULL "\xef\x95\x8c" // U+f54c +#define ICON_FA_SKULL_CROSSBONES "\xef\x9c\x94" // U+f714 +#define ICON_FA_SLASH "\xef\x9c\x95" // U+f715 +#define ICON_FA_SLEIGH "\xef\x9f\x8c" // U+f7cc +#define ICON_FA_SLIDERS_H "\xef\x87\x9e" // U+f1de +#define ICON_FA_SMILE "\xef\x84\x98" // U+f118 +#define ICON_FA_SMILE_BEAM "\xef\x96\xb8" // U+f5b8 +#define ICON_FA_SMILE_WINK "\xef\x93\x9a" // U+f4da +#define ICON_FA_SMOG "\xef\x9d\x9f" // U+f75f +#define ICON_FA_SMOKING "\xef\x92\x8d" // U+f48d +#define ICON_FA_SMOKING_BAN "\xef\x95\x8d" // U+f54d +#define ICON_FA_SMS "\xef\x9f\x8d" // U+f7cd +#define ICON_FA_SNOWBOARDING "\xef\x9f\x8e" // U+f7ce +#define ICON_FA_SNOWFLAKE "\xef\x8b\x9c" // U+f2dc +#define ICON_FA_SNOWMAN "\xef\x9f\x90" // U+f7d0 +#define ICON_FA_SNOWPLOW "\xef\x9f\x92" // U+f7d2 +#define ICON_FA_SOAP "\xee\x81\xae" // U+e06e +#define ICON_FA_SOCKS "\xef\x9a\x96" // U+f696 +#define ICON_FA_SOLAR_PANEL "\xef\x96\xba" // U+f5ba +#define ICON_FA_SORT "\xef\x83\x9c" // U+f0dc +#define ICON_FA_SORT_ALPHA_DOWN "\xef\x85\x9d" // U+f15d +#define ICON_FA_SORT_ALPHA_DOWN_ALT "\xef\xa2\x81" // U+f881 +#define ICON_FA_SORT_ALPHA_UP "\xef\x85\x9e" // U+f15e +#define ICON_FA_SORT_ALPHA_UP_ALT "\xef\xa2\x82" // U+f882 +#define ICON_FA_SORT_AMOUNT_DOWN "\xef\x85\xa0" // U+f160 +#define ICON_FA_SORT_AMOUNT_DOWN_ALT "\xef\xa2\x84" // U+f884 +#define ICON_FA_SORT_AMOUNT_UP "\xef\x85\xa1" // U+f161 +#define ICON_FA_SORT_AMOUNT_UP_ALT "\xef\xa2\x85" // U+f885 +#define ICON_FA_SORT_DOWN "\xef\x83\x9d" // U+f0dd +#define ICON_FA_SORT_NUMERIC_DOWN "\xef\x85\xa2" // U+f162 +#define ICON_FA_SORT_NUMERIC_DOWN_ALT "\xef\xa2\x86" // U+f886 +#define ICON_FA_SORT_NUMERIC_UP "\xef\x85\xa3" // U+f163 +#define ICON_FA_SORT_NUMERIC_UP_ALT "\xef\xa2\x87" // U+f887 +#define ICON_FA_SORT_UP "\xef\x83\x9e" // U+f0de +#define ICON_FA_SPA "\xef\x96\xbb" // U+f5bb +#define ICON_FA_SPACE_SHUTTLE "\xef\x86\x97" // U+f197 +#define ICON_FA_SPELL_CHECK "\xef\xa2\x91" // U+f891 +#define ICON_FA_SPIDER "\xef\x9c\x97" // U+f717 +#define ICON_FA_SPINNER "\xef\x84\x90" // U+f110 +#define ICON_FA_SPLOTCH "\xef\x96\xbc" // U+f5bc +#define ICON_FA_SPRAY_CAN "\xef\x96\xbd" // U+f5bd +#define ICON_FA_SQUARE "\xef\x83\x88" // U+f0c8 +#define ICON_FA_SQUARE_FULL "\xef\x91\x9c" // U+f45c +#define ICON_FA_SQUARE_ROOT_ALT "\xef\x9a\x98" // U+f698 +#define ICON_FA_STAMP "\xef\x96\xbf" // U+f5bf +#define ICON_FA_STAR "\xef\x80\x85" // U+f005 +#define ICON_FA_STAR_AND_CRESCENT "\xef\x9a\x99" // U+f699 +#define ICON_FA_STAR_HALF "\xef\x82\x89" // U+f089 +#define ICON_FA_STAR_HALF_ALT "\xef\x97\x80" // U+f5c0 +#define ICON_FA_STAR_OF_DAVID "\xef\x9a\x9a" // U+f69a +#define ICON_FA_STAR_OF_LIFE "\xef\x98\xa1" // U+f621 +#define ICON_FA_STEP_BACKWARD "\xef\x81\x88" // U+f048 +#define ICON_FA_STEP_FORWARD "\xef\x81\x91" // U+f051 +#define ICON_FA_STETHOSCOPE "\xef\x83\xb1" // U+f0f1 +#define ICON_FA_STICKY_NOTE "\xef\x89\x89" // U+f249 +#define ICON_FA_STOP "\xef\x81\x8d" // U+f04d +#define ICON_FA_STOP_CIRCLE "\xef\x8a\x8d" // U+f28d +#define ICON_FA_STOPWATCH "\xef\x8b\xb2" // U+f2f2 +#define ICON_FA_STOPWATCH_20 "\xee\x81\xaf" // U+e06f +#define ICON_FA_STORE "\xef\x95\x8e" // U+f54e +#define ICON_FA_STORE_ALT "\xef\x95\x8f" // U+f54f +#define ICON_FA_STORE_ALT_SLASH "\xee\x81\xb0" // U+e070 +#define ICON_FA_STORE_SLASH "\xee\x81\xb1" // U+e071 +#define ICON_FA_STREAM "\xef\x95\x90" // U+f550 +#define ICON_FA_STREET_VIEW "\xef\x88\x9d" // U+f21d +#define ICON_FA_STRIKETHROUGH "\xef\x83\x8c" // U+f0cc +#define ICON_FA_STROOPWAFEL "\xef\x95\x91" // U+f551 +#define ICON_FA_SUBSCRIPT "\xef\x84\xac" // U+f12c +#define ICON_FA_SUBWAY "\xef\x88\xb9" // U+f239 +#define ICON_FA_SUITCASE "\xef\x83\xb2" // U+f0f2 +#define ICON_FA_SUITCASE_ROLLING "\xef\x97\x81" // U+f5c1 +#define ICON_FA_SUN "\xef\x86\x85" // U+f185 +#define ICON_FA_SUPERSCRIPT "\xef\x84\xab" // U+f12b +#define ICON_FA_SURPRISE "\xef\x97\x82" // U+f5c2 +#define ICON_FA_SWATCHBOOK "\xef\x97\x83" // U+f5c3 +#define ICON_FA_SWIMMER "\xef\x97\x84" // U+f5c4 +#define ICON_FA_SWIMMING_POOL "\xef\x97\x85" // U+f5c5 +#define ICON_FA_SYNAGOGUE "\xef\x9a\x9b" // U+f69b +#define ICON_FA_SYNC "\xef\x80\xa1" // U+f021 +#define ICON_FA_SYNC_ALT "\xef\x8b\xb1" // U+f2f1 +#define ICON_FA_SYRINGE "\xef\x92\x8e" // U+f48e +#define ICON_FA_TABLE "\xef\x83\x8e" // U+f0ce +#define ICON_FA_TABLE_TENNIS "\xef\x91\x9d" // U+f45d +#define ICON_FA_TABLET "\xef\x84\x8a" // U+f10a +#define ICON_FA_TABLET_ALT "\xef\x8f\xba" // U+f3fa +#define ICON_FA_TABLETS "\xef\x92\x90" // U+f490 +#define ICON_FA_TACHOMETER_ALT "\xef\x8f\xbd" // U+f3fd +#define ICON_FA_TAG "\xef\x80\xab" // U+f02b +#define ICON_FA_TAGS "\xef\x80\xac" // U+f02c +#define ICON_FA_TAPE "\xef\x93\x9b" // U+f4db +#define ICON_FA_TASKS "\xef\x82\xae" // U+f0ae +#define ICON_FA_TAXI "\xef\x86\xba" // U+f1ba +#define ICON_FA_TEETH "\xef\x98\xae" // U+f62e +#define ICON_FA_TEETH_OPEN "\xef\x98\xaf" // U+f62f +#define ICON_FA_TEMPERATURE_HIGH "\xef\x9d\xa9" // U+f769 +#define ICON_FA_TEMPERATURE_LOW "\xef\x9d\xab" // U+f76b +#define ICON_FA_TENGE "\xef\x9f\x97" // U+f7d7 +#define ICON_FA_TERMINAL "\xef\x84\xa0" // U+f120 +#define ICON_FA_TEXT_HEIGHT "\xef\x80\xb4" // U+f034 +#define ICON_FA_TEXT_WIDTH "\xef\x80\xb5" // U+f035 +#define ICON_FA_TH "\xef\x80\x8a" // U+f00a +#define ICON_FA_TH_LARGE "\xef\x80\x89" // U+f009 +#define ICON_FA_TH_LIST "\xef\x80\x8b" // U+f00b +#define ICON_FA_THEATER_MASKS "\xef\x98\xb0" // U+f630 +#define ICON_FA_THERMOMETER "\xef\x92\x91" // U+f491 +#define ICON_FA_THERMOMETER_EMPTY "\xef\x8b\x8b" // U+f2cb +#define ICON_FA_THERMOMETER_FULL "\xef\x8b\x87" // U+f2c7 +#define ICON_FA_THERMOMETER_HALF "\xef\x8b\x89" // U+f2c9 +#define ICON_FA_THERMOMETER_QUARTER "\xef\x8b\x8a" // U+f2ca +#define ICON_FA_THERMOMETER_THREE_QUARTERS "\xef\x8b\x88" // U+f2c8 +#define ICON_FA_THUMBS_DOWN "\xef\x85\xa5" // U+f165 +#define ICON_FA_THUMBS_UP "\xef\x85\xa4" // U+f164 +#define ICON_FA_THUMBTACK "\xef\x82\x8d" // U+f08d +#define ICON_FA_TICKET_ALT "\xef\x8f\xbf" // U+f3ff +#define ICON_FA_TIMES "\xef\x80\x8d" // U+f00d +#define ICON_FA_TIMES_CIRCLE "\xef\x81\x97" // U+f057 +#define ICON_FA_TINT "\xef\x81\x83" // U+f043 +#define ICON_FA_TINT_SLASH "\xef\x97\x87" // U+f5c7 +#define ICON_FA_TIRED "\xef\x97\x88" // U+f5c8 +#define ICON_FA_TOGGLE_OFF "\xef\x88\x84" // U+f204 +#define ICON_FA_TOGGLE_ON "\xef\x88\x85" // U+f205 +#define ICON_FA_TOILET "\xef\x9f\x98" // U+f7d8 +#define ICON_FA_TOILET_PAPER "\xef\x9c\x9e" // U+f71e +#define ICON_FA_TOILET_PAPER_SLASH "\xee\x81\xb2" // U+e072 +#define ICON_FA_TOOLBOX "\xef\x95\x92" // U+f552 +#define ICON_FA_TOOLS "\xef\x9f\x99" // U+f7d9 +#define ICON_FA_TOOTH "\xef\x97\x89" // U+f5c9 +#define ICON_FA_TORAH "\xef\x9a\xa0" // U+f6a0 +#define ICON_FA_TORII_GATE "\xef\x9a\xa1" // U+f6a1 +#define ICON_FA_TRACTOR "\xef\x9c\xa2" // U+f722 +#define ICON_FA_TRADEMARK "\xef\x89\x9c" // U+f25c +#define ICON_FA_TRAFFIC_LIGHT "\xef\x98\xb7" // U+f637 +#define ICON_FA_TRAILER "\xee\x81\x81" // U+e041 +#define ICON_FA_TRAIN "\xef\x88\xb8" // U+f238 +#define ICON_FA_TRAM "\xef\x9f\x9a" // U+f7da +#define ICON_FA_TRANSGENDER "\xef\x88\xa4" // U+f224 +#define ICON_FA_TRANSGENDER_ALT "\xef\x88\xa5" // U+f225 +#define ICON_FA_TRASH "\xef\x87\xb8" // U+f1f8 +#define ICON_FA_TRASH_ALT "\xef\x8b\xad" // U+f2ed +#define ICON_FA_TRASH_RESTORE "\xef\xa0\xa9" // U+f829 +#define ICON_FA_TRASH_RESTORE_ALT "\xef\xa0\xaa" // U+f82a +#define ICON_FA_TREE "\xef\x86\xbb" // U+f1bb +#define ICON_FA_TROPHY "\xef\x82\x91" // U+f091 +#define ICON_FA_TRUCK "\xef\x83\x91" // U+f0d1 +#define ICON_FA_TRUCK_LOADING "\xef\x93\x9e" // U+f4de +#define ICON_FA_TRUCK_MONSTER "\xef\x98\xbb" // U+f63b +#define ICON_FA_TRUCK_MOVING "\xef\x93\x9f" // U+f4df +#define ICON_FA_TRUCK_PICKUP "\xef\x98\xbc" // U+f63c +#define ICON_FA_TSHIRT "\xef\x95\x93" // U+f553 +#define ICON_FA_TTY "\xef\x87\xa4" // U+f1e4 +#define ICON_FA_TV "\xef\x89\xac" // U+f26c +#define ICON_FA_UMBRELLA "\xef\x83\xa9" // U+f0e9 +#define ICON_FA_UMBRELLA_BEACH "\xef\x97\x8a" // U+f5ca +#define ICON_FA_UNDERLINE "\xef\x83\x8d" // U+f0cd +#define ICON_FA_UNDO "\xef\x83\xa2" // U+f0e2 +#define ICON_FA_UNDO_ALT "\xef\x8b\xaa" // U+f2ea +#define ICON_FA_UNIVERSAL_ACCESS "\xef\x8a\x9a" // U+f29a +#define ICON_FA_UNIVERSITY "\xef\x86\x9c" // U+f19c +#define ICON_FA_UNLINK "\xef\x84\xa7" // U+f127 +#define ICON_FA_UNLOCK "\xef\x82\x9c" // U+f09c +#define ICON_FA_UNLOCK_ALT "\xef\x84\xbe" // U+f13e +#define ICON_FA_UPLOAD "\xef\x82\x93" // U+f093 +#define ICON_FA_USER "\xef\x80\x87" // U+f007 +#define ICON_FA_USER_ALT "\xef\x90\x86" // U+f406 +#define ICON_FA_USER_ALT_SLASH "\xef\x93\xba" // U+f4fa +#define ICON_FA_USER_ASTRONAUT "\xef\x93\xbb" // U+f4fb +#define ICON_FA_USER_CHECK "\xef\x93\xbc" // U+f4fc +#define ICON_FA_USER_CIRCLE "\xef\x8a\xbd" // U+f2bd +#define ICON_FA_USER_CLOCK "\xef\x93\xbd" // U+f4fd +#define ICON_FA_USER_COG "\xef\x93\xbe" // U+f4fe +#define ICON_FA_USER_EDIT "\xef\x93\xbf" // U+f4ff +#define ICON_FA_USER_FRIENDS "\xef\x94\x80" // U+f500 +#define ICON_FA_USER_GRADUATE "\xef\x94\x81" // U+f501 +#define ICON_FA_USER_INJURED "\xef\x9c\xa8" // U+f728 +#define ICON_FA_USER_LOCK "\xef\x94\x82" // U+f502 +#define ICON_FA_USER_MD "\xef\x83\xb0" // U+f0f0 +#define ICON_FA_USER_MINUS "\xef\x94\x83" // U+f503 +#define ICON_FA_USER_NINJA "\xef\x94\x84" // U+f504 +#define ICON_FA_USER_NURSE "\xef\xa0\xaf" // U+f82f +#define ICON_FA_USER_PLUS "\xef\x88\xb4" // U+f234 +#define ICON_FA_USER_SECRET "\xef\x88\x9b" // U+f21b +#define ICON_FA_USER_SHIELD "\xef\x94\x85" // U+f505 +#define ICON_FA_USER_SLASH "\xef\x94\x86" // U+f506 +#define ICON_FA_USER_TAG "\xef\x94\x87" // U+f507 +#define ICON_FA_USER_TIE "\xef\x94\x88" // U+f508 +#define ICON_FA_USER_TIMES "\xef\x88\xb5" // U+f235 +#define ICON_FA_USERS "\xef\x83\x80" // U+f0c0 +#define ICON_FA_USERS_COG "\xef\x94\x89" // U+f509 +#define ICON_FA_USERS_SLASH "\xee\x81\xb3" // U+e073 +#define ICON_FA_UTENSIL_SPOON "\xef\x8b\xa5" // U+f2e5 +#define ICON_FA_UTENSILS "\xef\x8b\xa7" // U+f2e7 +#define ICON_FA_VECTOR_SQUARE "\xef\x97\x8b" // U+f5cb +#define ICON_FA_VENUS "\xef\x88\xa1" // U+f221 +#define ICON_FA_VENUS_DOUBLE "\xef\x88\xa6" // U+f226 +#define ICON_FA_VENUS_MARS "\xef\x88\xa8" // U+f228 +#define ICON_FA_VEST "\xee\x82\x85" // U+e085 +#define ICON_FA_VEST_PATCHES "\xee\x82\x86" // U+e086 +#define ICON_FA_VIAL "\xef\x92\x92" // U+f492 +#define ICON_FA_VIALS "\xef\x92\x93" // U+f493 +#define ICON_FA_VIDEO "\xef\x80\xbd" // U+f03d +#define ICON_FA_VIDEO_SLASH "\xef\x93\xa2" // U+f4e2 +#define ICON_FA_VIHARA "\xef\x9a\xa7" // U+f6a7 +#define ICON_FA_VIRUS "\xee\x81\xb4" // U+e074 +#define ICON_FA_VIRUS_SLASH "\xee\x81\xb5" // U+e075 +#define ICON_FA_VIRUSES "\xee\x81\xb6" // U+e076 +#define ICON_FA_VOICEMAIL "\xef\xa2\x97" // U+f897 +#define ICON_FA_VOLLEYBALL_BALL "\xef\x91\x9f" // U+f45f +#define ICON_FA_VOLUME_DOWN "\xef\x80\xa7" // U+f027 +#define ICON_FA_VOLUME_MUTE "\xef\x9a\xa9" // U+f6a9 +#define ICON_FA_VOLUME_OFF "\xef\x80\xa6" // U+f026 +#define ICON_FA_VOLUME_UP "\xef\x80\xa8" // U+f028 +#define ICON_FA_VOTE_YEA "\xef\x9d\xb2" // U+f772 +#define ICON_FA_VR_CARDBOARD "\xef\x9c\xa9" // U+f729 +#define ICON_FA_WALKING "\xef\x95\x94" // U+f554 +#define ICON_FA_WALLET "\xef\x95\x95" // U+f555 +#define ICON_FA_WAREHOUSE "\xef\x92\x94" // U+f494 +#define ICON_FA_WATER "\xef\x9d\xb3" // U+f773 +#define ICON_FA_WAVE_SQUARE "\xef\xa0\xbe" // U+f83e +#define ICON_FA_WEIGHT "\xef\x92\x96" // U+f496 +#define ICON_FA_WEIGHT_HANGING "\xef\x97\x8d" // U+f5cd +#define ICON_FA_WHEELCHAIR "\xef\x86\x93" // U+f193 +#define ICON_FA_WIFI "\xef\x87\xab" // U+f1eb +#define ICON_FA_WIND "\xef\x9c\xae" // U+f72e +#define ICON_FA_WINDOW_CLOSE "\xef\x90\x90" // U+f410 +#define ICON_FA_WINDOW_MAXIMIZE "\xef\x8b\x90" // U+f2d0 +#define ICON_FA_WINDOW_MINIMIZE "\xef\x8b\x91" // U+f2d1 +#define ICON_FA_WINDOW_RESTORE "\xef\x8b\x92" // U+f2d2 +#define ICON_FA_WINE_BOTTLE "\xef\x9c\xaf" // U+f72f +#define ICON_FA_WINE_GLASS "\xef\x93\xa3" // U+f4e3 +#define ICON_FA_WINE_GLASS_ALT "\xef\x97\x8e" // U+f5ce +#define ICON_FA_WON_SIGN "\xef\x85\x99" // U+f159 +#define ICON_FA_WRENCH "\xef\x82\xad" // U+f0ad +#define ICON_FA_X_RAY "\xef\x92\x97" // U+f497 +#define ICON_FA_YEN_SIGN "\xef\x85\x97" // U+f157 +#define ICON_FA_YIN_YANG "\xef\x9a\xad" // U+f6ad diff --git a/src/frontend-common/common_host_interface.cpp b/src/frontend-common/common_host_interface.cpp index 71dd12c20..007a24b43 100644 --- a/src/frontend-common/common_host_interface.cpp +++ b/src/frontend-common/common_host_interface.cpp @@ -21,6 +21,7 @@ #include "core/texture_replacements.h" #include "core/timers.h" #include "cubeb_audio_stream.h" +#include "fullscreen_ui.h" #include "game_list.h" #include "icon.h" #include "imgui.h" @@ -786,7 +787,7 @@ void CommonHostInterface::OnSystemPaused(bool paused) if (paused) { - if (IsFullscreen()) + if (IsFullscreen() && !m_fullscreen_ui_enabled) SetFullscreen(false); StopControllerRumble(); @@ -827,6 +828,12 @@ void CommonHostInterface::OnControllerTypeChanged(u32 slot) void CommonHostInterface::DrawImGuiWindows() { + if (m_fullscreen_ui_enabled) + { + FullscreenUI::Render(); + return; + } + if (System::IsValid()) { DrawDebugWindows(); @@ -1569,6 +1576,11 @@ void CommonHostInterface::RegisterHotkeys() void CommonHostInterface::RegisterGeneralHotkeys() { + RegisterHotkey(StaticString(TRANSLATABLE("Hotkeys", "General")), StaticString("OpenQuickMenu"), + TRANSLATABLE("Hotkeys", "Open Quick Menu"), [this](bool pressed) { + if (pressed && m_fullscreen_ui_enabled) + FullscreenUI::OpenQuickMenu(); + }); RegisterHotkey(StaticString(TRANSLATABLE("Hotkeys", "General")), StaticString("FastForward"), TRANSLATABLE("Hotkeys", "Fast Forward"), [this](bool pressed) { SetFastForwardEnabled(pressed); }); @@ -2869,7 +2881,10 @@ std::unique_ptr CommonHostInterface::OpenPackageFile(const char* pat bool CommonHostInterface::SetControllerNavigationButtonState(FrontendCommon::ControllerNavigationButton button, bool pressed) { - return false; + if (!m_fullscreen_ui_enabled) + return false; + + return FullscreenUI::SetControllerNavInput(button, pressed); } #ifdef WITH_DISCORD_PRESENCE diff --git a/src/frontend-common/common_host_interface.h b/src/frontend-common/common_host_interface.h index 9dd43c1b1..c950d7eaa 100644 --- a/src/frontend-common/common_host_interface.h +++ b/src/frontend-common/common_host_interface.h @@ -130,6 +130,9 @@ public: /// Returns true if running in batch mode, i.e. exit after emulation. ALWAYS_INLINE bool InBatchMode() const { return m_command_line_flags.batch_mode; } + /// Returns true if the fullscreen UI is enabled. + ALWAYS_INLINE bool IsFullscreenUIEnabled() const { return m_fullscreen_ui_enabled; } + /// Parses command line parameters for all frontends. bool ParseCommandLineParameters(int argc, char* argv[], std::unique_ptr* out_boot_params); @@ -383,6 +386,7 @@ protected: std::deque m_osd_messages; std::mutex m_osd_messages_lock; + bool m_fullscreen_ui_enabled = false; bool m_frame_step_request = false; bool m_fast_forward_enabled = false; bool m_turbo_enabled = false; @@ -390,6 +394,17 @@ protected: bool m_throttler_enabled = true; bool m_display_all_frames = true; + union + { + u8 bits; + + // running in batch mode? i.e. exit after stopping emulation + BitField batch_mode; + + // disable controller interface (buggy devices with SDL) + BitField disable_controller_interface; + } m_command_line_flags = {}; + private: void InitializeUserDirectory(); void RegisterGeneralHotkeys(); @@ -434,17 +449,6 @@ private: }; std::vector m_controller_vibration_motors; - union - { - u8 bits; - - // running in batch mode? i.e. exit after stopping emulation - BitField batch_mode; - - // disable controller interface (buggy devices with SDL) - BitField disable_controller_interface; - } m_command_line_flags = {}; - #ifdef WITH_DISCORD_PRESENCE // discord rich presence bool m_discord_presence_enabled = false; diff --git a/src/frontend-common/frontend-common.vcxproj b/src/frontend-common/frontend-common.vcxproj index 9b5b26760..cd93015fa 100644 --- a/src/frontend-common/frontend-common.vcxproj +++ b/src/frontend-common/frontend-common.vcxproj @@ -88,9 +88,12 @@ + + + @@ -113,9 +116,12 @@ + + + diff --git a/src/frontend-common/frontend-common.vcxproj.filters b/src/frontend-common/frontend-common.vcxproj.filters index 42615ac11..2665afae2 100644 --- a/src/frontend-common/frontend-common.vcxproj.filters +++ b/src/frontend-common/frontend-common.vcxproj.filters @@ -24,6 +24,9 @@ + + + @@ -49,6 +52,9 @@ + + + diff --git a/src/frontend-common/fullscreen_ui.cpp b/src/frontend-common/fullscreen_ui.cpp new file mode 100644 index 000000000..62f35df88 --- /dev/null +++ b/src/frontend-common/fullscreen_ui.cpp @@ -0,0 +1,3341 @@ +#define IMGUI_DEFINE_MATH_OPERATORS + +#include "fullscreen_ui.h" +#include "IconsFontAwesome5.h" +#include "common/byte_stream.h" +#include "common/file_system.h" +#include "common/log.h" +#include "common/make_array.h" +#include "common/string.h" +#include "common/string_util.h" +#include "common_host_interface.h" +#include "controller_interface.h" +#include "core/cheats.h" +#include "core/cpu_core.h" +#include "core/gpu.h" +#include "core/host_display.h" +#include "core/host_interface_progress_callback.h" +#include "core/resources.h" +#include "core/settings.h" +#include "core/system.h" +#include "fullscreen_ui_progress_callback.h" +#include "game_list.h" +#include "icon.h" +#include "imgui.h" +#include "imgui_fullscreen.h" +#include "imgui_internal.h" +#include "imgui_stdlib.h" +#include "imgui_styles.h" +#include "scmversion/scmversion.h" +#include +#include +Log_SetChannel(FullscreenUI); + +static constexpr float LAYOUT_MAIN_MENU_BAR_SIZE = 20.0f; // Should be DPI scaled, not layout scaled! + +using ImGuiFullscreen::g_large_font; +using ImGuiFullscreen::g_medium_font; +using ImGuiFullscreen::LAYOUT_LARGE_FONT_SIZE; +using ImGuiFullscreen::LAYOUT_MEDIUM_FONT_SIZE; +using ImGuiFullscreen::LAYOUT_SCREEN_HEIGHT; +using ImGuiFullscreen::LAYOUT_SCREEN_WIDTH; + +using ImGuiFullscreen::ActiveButton; +using ImGuiFullscreen::BeginFullscreenColumns; +using ImGuiFullscreen::BeginFullscreenColumnWindow; +using ImGuiFullscreen::BeginFullscreenWindow; +using ImGuiFullscreen::BeginMenuButtons; +using ImGuiFullscreen::CloseChoiceDialog; +using ImGuiFullscreen::CloseFileSelector; +using ImGuiFullscreen::DPIScale; +using ImGuiFullscreen::EndFullscreenColumns; +using ImGuiFullscreen::EndFullscreenColumnWindow; +using ImGuiFullscreen::EndFullscreenWindow; +using ImGuiFullscreen::EndMenuButtons; +using ImGuiFullscreen::EnumChoiceButton; +using ImGuiFullscreen::LayoutScale; +using ImGuiFullscreen::MenuButton; +using ImGuiFullscreen::MenuButtonFrame; +using ImGuiFullscreen::MenuButtonWithValue; +using ImGuiFullscreen::MenuHeading; +using ImGuiFullscreen::MenuImageButton; +using ImGuiFullscreen::OpenChoiceDialog; +using ImGuiFullscreen::OpenFileSelector; +using ImGuiFullscreen::RangeButton; +using ImGuiFullscreen::ToggleButton; + +namespace FullscreenUI { + +////////////////////////////////////////////////////////////////////////// +// Main +////////////////////////////////////////////////////////////////////////// +static void ClearImGuiFocus(); +static void ReturnToMainWindow(); +static void DrawLandingWindow(); +static void DrawQuickMenu(MainWindowType type); +static void DrawDebugMenu(); +static void DrawStatsOverlay(); +static void DrawOSDMessages(); +static void DrawAboutWindow(); +static void OpenAboutWindow(); + +static CommonHostInterface* s_host_interface; +static SettingsInterface* s_settings_interface; +static MainWindowType s_current_main_window = MainWindowType::Landing; +static std::bitset(FrontendCommon::ControllerNavigationButton::Count)> s_nav_input_values{}; +static bool s_debug_menu_enabled = false; +static bool s_quick_menu_was_open = false; +static bool s_was_paused_on_quick_menu_open = false; +static bool s_about_window_open = false; + +////////////////////////////////////////////////////////////////////////// +// Resources +////////////////////////////////////////////////////////////////////////// +static std::unique_ptr LoadTextureResource(const char* name); +static bool LoadResources(); +static void DestroyResources(); + +std::unique_ptr s_app_icon_texture; +std::unique_ptr s_placeholder_texture; +std::array, static_cast(DiscRegion::Count)> s_disc_region_textures; +std::array, static_cast(GameListCompatibilityRating::Count)> + s_game_compatibility_textures; +std::unique_ptr s_fallback_disc_texture; +std::unique_ptr s_fallback_exe_texture; +std::unique_ptr s_fallback_psf_texture; +std::unique_ptr s_fallback_playlist_texture; + +////////////////////////////////////////////////////////////////////////// +// Settings +////////////////////////////////////////////////////////////////////////// + +enum class InputBindingType +{ + None, + Button, + Axis, + Rumble +}; + +static constexpr double INPUT_BINDING_TIMEOUT_SECONDS = 5.0; + +static void DrawSettingsWindow(); +static void BeginInputBinding(InputBindingType type, const std::string_view& section, const std::string_view& key, + const std::string_view& display_name); +static void EndInputBinding(); +static void DrawInputBindingWindow(); + +static SettingsPage s_settings_page = SettingsPage::InterfaceSettings; +static Settings s_settings_copy; +static InputBindingType s_input_binding_type = InputBindingType::None; +static TinyString s_input_binding_section; +static TinyString s_input_binding_key; +static TinyString s_input_binding_display_name; +static Common::Timer s_input_binding_timer; + +////////////////////////////////////////////////////////////////////////// +// Save State List +////////////////////////////////////////////////////////////////////////// +struct SaveStateListEntry +{ + std::string title; + std::string summary; + std::string path; + std::string media_path; + std::unique_ptr preview_texture; + s32 slot; + bool global; +}; + +static void InitializePlaceholderSaveStateListEntry(SaveStateListEntry* li, s32 slot, bool global); +static void InitializeSaveStateListEntry(SaveStateListEntry* li, CommonHostInterface::ExtendedSaveStateInfo* ssi); +static void PopulateSaveStateListEntries(); +static void OpenSaveStateSelector(bool is_loading); +static void CloseSaveStateSelector(); +static void DrawSaveStateSelector(bool is_loading, bool fullscreen); + +static std::vector s_save_state_selector_slots; +static bool s_save_state_selector_open = false; +static bool s_save_state_selector_loading = true; + +////////////////////////////////////////////////////////////////////////// +// Game List +////////////////////////////////////////////////////////////////////////// +static void DrawGameListWindow(); +static void SwitchToGameList(); +static void QueueGameListRefresh(); +static void SortGameList(); +static HostDisplayTexture* GetTextureForGameListEntryType(GameListEntryType type); +static HostDisplayTexture* GetGameListCover(const GameListEntry* entry); +static HostDisplayTexture* GetCoverForCurrentGame(); + +// Lazily populated cover images. +static std::unordered_map> s_cover_image_map; +static std::vector s_game_list_sorted_entries; +static std::thread s_game_list_load_thread; + +////////////////////////////////////////////////////////////////////////// +// Main +////////////////////////////////////////////////////////////////////////// + +bool Initialize(CommonHostInterface* host_interface, SettingsInterface* settings_interface) +{ + s_host_interface = host_interface; + s_settings_interface = settings_interface; + if (!LoadResources()) + return false; + + s_settings_copy.Load(*settings_interface); + SetDebugMenuEnabled(settings_interface->GetBoolValue("Main", "ShowDebugMenu", false)); + QueueGameListRefresh(); + + ImGuiFullscreen::UpdateLayoutScale(); + ImGuiFullscreen::UpdateFonts(); + + return true; +} + +bool HasActiveWindow() +{ + return s_current_main_window != MainWindowType::None; +} + +void SystemCreated() +{ + s_current_main_window = MainWindowType::None; + ClearImGuiFocus(); +} + +void SystemDestroyed() +{ + s_current_main_window = MainWindowType::Landing; + s_quick_menu_was_open = false; + ClearImGuiFocus(); +} + +void SystemPaused(bool paused) +{ + // +} + +void OpenQuickMenu() +{ + if (!System::IsValid()) + return; + + s_was_paused_on_quick_menu_open = System::IsPaused(); + if (s_settings_copy.pause_on_focus_loss && !s_was_paused_on_quick_menu_open) + s_host_interface->RunLater([]() { s_host_interface->PauseSystem(true); }); + + s_current_main_window = MainWindowType::QuickMenu; + s_quick_menu_was_open = true; + ClearImGuiFocus(); +} + +void CloseQuickMenu() +{ + if (!System::IsValid()) + return; + + if (System::IsPaused() && !s_was_paused_on_quick_menu_open) + s_host_interface->RunLater([]() { s_host_interface->PauseSystem(false); }); + + s_current_main_window = MainWindowType::None; + s_quick_menu_was_open = false; + ClearImGuiFocus(); +} + +void Shutdown() +{ + if (s_game_list_load_thread.joinable()) + s_game_list_load_thread.join(); + + CloseSaveStateSelector(); + s_cover_image_map.clear(); + s_nav_input_values = {}; + DestroyResources(); + + s_settings_interface = nullptr; + s_host_interface = nullptr; +} + +void Render() +{ + if (s_debug_menu_enabled) + DrawDebugMenu(); + else if (System::IsValid()) + DrawStatsOverlay(); + + ImGuiFullscreen::BeginLayout(); + + switch (s_current_main_window) + { + case MainWindowType::Landing: + DrawLandingWindow(); + break; + case MainWindowType::GameList: + DrawGameListWindow(); + break; + case MainWindowType::Settings: + DrawSettingsWindow(); + break; + case MainWindowType::QuickMenu: + case MainWindowType::MoreQuickMenu: + DrawQuickMenu(s_current_main_window); + break; + default: + break; + } + + if (s_save_state_selector_open) + DrawSaveStateSelector(s_save_state_selector_loading, false); + + if (s_about_window_open) + DrawAboutWindow(); + + if (s_input_binding_type != InputBindingType::None) + DrawInputBindingWindow(); + + ImGuiFullscreen::EndLayout(); + + DrawOSDMessages(); +} + +Settings& GetSettingsCopy() +{ + return s_settings_copy; +} + +void SaveAndApplySettings() +{ + s_settings_copy.Save(*s_settings_interface); + s_settings_interface->Save(); + s_host_interface->ApplySettings(false); +} + +void ClearImGuiFocus() +{ + ImGui::SetWindowFocus(nullptr); +} + +void ReturnToMainWindow() +{ + if (System::IsValid()) + s_current_main_window = s_quick_menu_was_open ? MainWindowType::QuickMenu : MainWindowType::None; + else + s_current_main_window = MainWindowType::Landing; +} + +bool LoadResources() +{ + if (!(s_app_icon_texture = LoadTextureResource("logo.png")) && + !(s_app_icon_texture = LoadTextureResource("duck.png"))) + { + return false; + } + + if (!(s_placeholder_texture = s_host_interface->GetDisplay()->CreateTexture( + PLACEHOLDER_ICON_WIDTH, PLACEHOLDER_ICON_HEIGHT, 1, 1, 1, HostDisplayPixelFormat::RGBA8, + PLACEHOLDER_ICON_DATA, sizeof(u32) * PLACEHOLDER_ICON_WIDTH, false))) + { + return false; + } + + if (!(s_disc_region_textures[static_cast(DiscRegion::NTSC_U)] = LoadTextureResource("flag-uc.png")) || + !(s_disc_region_textures[static_cast(DiscRegion::NTSC_J)] = LoadTextureResource("flag-jp.png")) || + !(s_disc_region_textures[static_cast(DiscRegion::PAL)] = LoadTextureResource("flag-eu.png")) || + !(s_disc_region_textures[static_cast(DiscRegion::Other)] = LoadTextureResource("flag-eu.png")) || + !(s_fallback_disc_texture = LoadTextureResource("media-cdrom.png")) || + !(s_fallback_exe_texture = LoadTextureResource("applications-system.png")) || + !(s_fallback_psf_texture = LoadTextureResource("multimedia-player.png")) || + !(s_fallback_playlist_texture = LoadTextureResource("address-book-new.png"))) + { + return false; + } + + for (u32 i = 0; i < static_cast(GameListCompatibilityRating::Count); i++) + { + if (!(s_game_compatibility_textures[i] = LoadTextureResource(TinyString::FromFormat("star-%u.png", i)))) + return false; + } + + { + std::unique_ptr stream = s_host_interface->OpenPackageFile( + "resources" FS_OSPATH_SEPARATOR_STR "fa-solid-900.ttf", BYTESTREAM_OPEN_READ | BYTESTREAM_OPEN_STREAMED); + if (!stream) + return false; + + std::vector font_data = FileSystem::ReadBinaryStream(stream.get()); + if (font_data.empty()) + return false; + + ImGuiFullscreen::SetIconFontData(std::move(font_data)); + } + + return true; +} + +void DestroyResources() +{ + s_app_icon_texture.reset(); + s_placeholder_texture.reset(); + for (auto& tex : s_game_compatibility_textures) + tex.reset(); + for (auto& tex : s_disc_region_textures) + tex.reset(); +} + +std::unique_ptr LoadTextureResource(const char* name) +{ + std::unique_ptr texture; + + const std::string path(StringUtil::StdStringFromFormat("resources" FS_OSPATH_SEPARATOR_STR "%s", name)); + std::unique_ptr stream = s_host_interface->OpenPackageFile(path.c_str(), BYTESTREAM_OPEN_READ); + if (!stream) + { + Log_ErrorPrintf("Failed to open texture resource '%s'", path.c_str()); + return {}; + } + + Common::RGBA8Image image; + if (Common::LoadImageFromStream(&image, stream.get()) && image.IsValid()) + { + texture = s_host_interface->GetDisplay()->CreateTexture(image.GetWidth(), image.GetHeight(), 1, 1, 1, + HostDisplayPixelFormat::RGBA8, image.GetPixels(), + image.GetByteStride()); + if (texture) + { + Log_DevPrintf("Uploaded texture resource '%s' (%ux%u)", name, image.GetWidth(), image.GetHeight()); + return texture; + } + + Log_ErrorPrintf("failed to create %ux%u texture for resource", image.GetWidth(), image.GetHeight()); + } + + Log_ErrorPrintf("Missing resource '%s', using fallback", name); + + texture = s_host_interface->GetDisplay()->CreateTexture(PLACEHOLDER_ICON_WIDTH, PLACEHOLDER_ICON_HEIGHT, 1, 1, 1, + HostDisplayPixelFormat::RGBA8, PLACEHOLDER_ICON_DATA, + sizeof(u32) * PLACEHOLDER_ICON_WIDTH, false); + if (!texture) + Panic("Failed to create placeholder texture"); + + return texture; +} + +////////////////////////////////////////////////////////////////////////// +// Utility +////////////////////////////////////////////////////////////////////////// + +static ImGuiFullscreen::FileSelectorFilters GetDiscImageFilters() +{ + return {"*.bin", "*.cue", "*.iso", "*.img", "*.chd", "*.psexe", "*.exe", "*.psf", "*.minipsf", "*.m3u"}; +} + +static void DoStartPath(const std::string& path, bool allow_resume) +{ + // we can never resume from exe/psf + if (System::IsExeFileName(path.c_str()) || System::IsPsfFileName(path.c_str())) + allow_resume = false; + + if (allow_resume && g_settings.save_state_on_exit) + { + s_host_interface->ResumeSystemFromState(path.c_str(), true); + return; + } + + SystemBootParameters params; + params.filename = path; + s_host_interface->BootSystem(params); +} + +static void DoStartFile() +{ + auto callback = [](const std::string& path) { + if (!path.empty()) + DoStartPath(path, false); + + ClearImGuiFocus(); + CloseFileSelector(); + }; + + OpenFileSelector(ICON_FA_COMPACT_DISC " Select Disc Image", false, std::move(callback), GetDiscImageFilters()); +} + +static void DoStartBIOS() +{ + s_host_interface->RunLater([]() { + SystemBootParameters boot_params; + s_host_interface->BootSystem(boot_params); + }); + ClearImGuiFocus(); +} + +static void DoPowerOff() +{ + s_host_interface->RunLater([]() { + if (!System::IsValid()) + return; + + if (g_settings.save_state_on_exit) + s_host_interface->SaveResumeSaveState(); + s_host_interface->PowerOffSystem(); + + ReturnToMainWindow(); + }); + ClearImGuiFocus(); +} + +static void DoReset() +{ + s_host_interface->RunLater([]() { + if (!System::IsValid()) + return; + + s_host_interface->ResetSystem(); + }); +} + +static void DoPause() +{ + s_host_interface->RunLater([]() { + if (!System::IsValid()) + return; + + s_host_interface->PauseSystem(!System::IsPaused()); + }); +} + +static void DoCheatsMenu() +{ + CheatList* cl = System::GetCheatList(); + if (!cl) + { + if (!s_host_interface->LoadCheatListFromDatabase() || !(cl = System::GetCheatList())) + { + s_host_interface->AddFormattedOSDMessage(10.0f, "No cheats found for %s.", System::GetRunningTitle().c_str()); + return; + } + } + + ImGuiFullscreen::ChoiceDialogOptions options; + options.reserve(cl->GetCodeCount()); + for (u32 i = 0; i < cl->GetCodeCount(); i++) + { + const CheatCode& cc = cl->GetCode(i); + options.emplace_back(cc.description.c_str(), cc.enabled); + } + + auto callback = [](s32 index, const std::string& title, bool checked) { + if (index < 0) + return; + + CheatList* cl = System::GetCheatList(); + if (!cl) + return; + + const CheatCode& cc = cl->GetCode(static_cast(index)); + if (cc.activation == CheatCode::Activation::Manual) + cl->ApplyCode(static_cast(index)); + else + s_host_interface->SetCheatCodeState(static_cast(index), checked, true); + }; + OpenChoiceDialog(ICON_FA_FROWN " Cheat List", true, std::move(options), std::move(callback)); +} + +static void DoToggleAnalogMode() +{ + // hacky way to toggle analog mode + for (u32 i = 0; i < NUM_CONTROLLER_AND_CARD_PORTS; i++) + { + Controller* ctrl = System::GetController(i); + if (!ctrl) + continue; + + std::optional code = Controller::GetButtonCodeByName(ctrl->GetType(), "Analog"); + if (!code.has_value()) + continue; + + ctrl->SetButtonState(code.value(), true); + ctrl->SetButtonState(code.value(), false); + } +} + +static void DoChangeDiscFromFile() +{ + auto callback = [](const std::string& path) { + if (!path.empty()) + System::InsertMedia(path.c_str()); + + ClearImGuiFocus(); + CloseFileSelector(); + }; + + OpenFileSelector(ICON_FA_COMPACT_DISC " Select Disc Image", false, std::move(callback), GetDiscImageFilters(), + FileSystem::GetPathDirectory(System::GetMediaFileName().c_str())); +} + +static void DoChangeDisc() +{ + const u32 playlist_count = System::GetMediaPlaylistCount(); + if (playlist_count == 0) + { + DoChangeDiscFromFile(); + return; + } + + const u32 current_index = (playlist_count > 0) ? System::GetMediaPlaylistIndex() : 0; + ImGuiFullscreen::ChoiceDialogOptions options; + options.reserve(playlist_count + 1); + options.emplace_back("From File...", false); + + for (u32 i = 0; i < playlist_count; i++) + options.emplace_back(System::GetMediaPlaylistPath(i), i == current_index); + + auto callback = [](s32 index, const std::string& title, bool checked) { + if (index < 0) + return; + if (index == 0) + { + DoChangeDiscFromFile(); + return; + } + + System::SwitchMediaFromPlaylist(static_cast(index - 1)); + CloseChoiceDialog(); + }; + + OpenChoiceDialog(ICON_FA_LIST, true, std::move(options), std::move(callback)); +} + +////////////////////////////////////////////////////////////////////////// +// Landing Window +////////////////////////////////////////////////////////////////////////// + +void DrawLandingWindow() +{ + BeginFullscreenColumns(); + + if (BeginFullscreenColumnWindow(0.0f, 570.0f, "logo", ImVec4(0.11f, 0.15f, 0.17f, 1.00f))) + { + ImGui::SetCursorPos(LayoutScale(ImVec2(120.0f, 170.0f))); + ImGui::Image(s_app_icon_texture->GetHandle(), LayoutScale(ImVec2(380.0f, 380.0f))); + } + EndFullscreenColumnWindow(); + + if (BeginFullscreenColumnWindow(570.0f, LAYOUT_SCREEN_WIDTH, "menu")) + { + BeginMenuButtons(7, 0.5f); + + if (MenuButton(" " ICON_FA_PLAY_CIRCLE " Resume", + "Starts the console from where it was before it was last closed.")) + { + s_host_interface->RunLater([]() { s_host_interface->ResumeSystemFromMostRecentState(); }); + ClearImGuiFocus(); + } + + if (MenuButton(" " ICON_FA_LIST " Open Game List", + "Launch a game from images scanned from your game directories.")) + { + s_host_interface->RunLater(SwitchToGameList); + } + + if (MenuButton(" " ICON_FA_FOLDER_OPEN " Start File", "Launch a game by selecting a file/disc image.")) + s_host_interface->RunLater(DoStartFile); + + if (MenuButton(" " ICON_FA_TOOLBOX " Start BIOS", "Start the console without any disc inserted.")) + s_host_interface->RunLater(DoStartBIOS); + + if (MenuButton(" " ICON_FA_UNDO " Load State", "Loads a global save state.")) + { + OpenSaveStateSelector(true); + } + + if (MenuButton(" " ICON_FA_SLIDERS_H " Settings", "Change settings for the emulator.")) + s_current_main_window = MainWindowType::Settings; + + if (MenuButton(" " ICON_FA_SIGN_OUT_ALT " Exit", "Exits the program.")) + s_host_interface->RequestExit(); + + { + bool about_visible, about_hovered, about_pressed; + ImRect about_rect; + ImGui::SetCursorPosY(LayoutScale(670.0f)); + about_pressed = MenuButtonFrame("About", true, ImGuiFullscreen::LAYOUT_MENU_BUTTON_HEIGHT_NO_SUMMARY, + &about_visible, &about_hovered, &about_rect.Min, &about_rect.Max); + + if (about_visible) + { + ImGui::PushFont(g_large_font); + ImGui::RenderTextClipped(about_rect.Min, about_rect.Max, ICON_FA_QUESTION_CIRCLE, nullptr, nullptr, + ImVec2(1.0f, 0.0f), &about_rect); + ImGui::PopFont(); + } + + if (about_pressed) + OpenAboutWindow(); + } + + EndMenuButtons(); + } + + EndFullscreenColumnWindow(); + + EndFullscreenColumns(); +} + +static ImGuiFullscreen::ChoiceDialogOptions GetGameListDirectoryOptions(bool recursive_as_checked) +{ + ImGuiFullscreen::ChoiceDialogOptions options; + + for (std::string& dir : s_settings_interface->GetStringList("GameList", "Paths")) + options.emplace_back(std::move(dir), false); + + for (std::string& dir : s_settings_interface->GetStringList("GameList", "RecursivePaths")) + options.emplace_back(std::move(dir), recursive_as_checked); + + std::sort(options.begin(), options.end(), [](const auto& lhs, const auto& rhs) { + return (StringUtil::Strcasecmp(lhs.first.c_str(), rhs.first.c_str()) < 0); + }); + + return options; +} + +static void DrawInputBindingButton(InputBindingType type, const char* section, const char* name, + const char* display_name) +{ + TinyString title; + title.Format("%s/%s", section, name); + + ImRect bb; + bool visible, hovered, clicked; + clicked = + MenuButtonFrame(title, true, ImGuiFullscreen::LAYOUT_MENU_BUTTON_HEIGHT, &visible, &hovered, &bb.Min, &bb.Max); + if (!visible) + return; + + const float midpoint = bb.Min.y + g_large_font->FontSize + LayoutScale(4.0f); + const ImRect title_bb(bb.Min, ImVec2(bb.Max.x, midpoint)); + const ImRect summary_bb(ImVec2(bb.Min.x, midpoint), bb.Max); + + switch (type) + { + case InputBindingType::Button: + title.Format(ICON_FA_CIRCLE " %s Button", display_name); + break; + case InputBindingType::Axis: + title.Format(ICON_FA_BULLSEYE " %s Axis", display_name); + break; + case InputBindingType::Rumble: + title.Format(ICON_FA_BELL " %s", display_name); + break; + default: + title = display_name; + break; + } + ImGui::PushFont(g_large_font); + ImGui::RenderTextClipped(title_bb.Min, title_bb.Max, title, nullptr, nullptr, ImVec2(0.0f, 0.0f), &title_bb); + ImGui::PopFont(); + + // eek, potential heap allocation :/ + const std::string value = s_settings_interface->GetStringValue(section, name); + ImGui::PushFont(g_medium_font); + ImGui::RenderTextClipped(summary_bb.Min, summary_bb.Max, value.empty() ? "(No Binding)" : value.c_str(), nullptr, + nullptr, ImVec2(0.0f, 0.0f), &summary_bb); + ImGui::PopFont(); + + if (clicked) + BeginInputBinding(type, section, name, display_name); +} + +static void ClearInputBindingVariables() +{ + s_input_binding_type = InputBindingType::None; + s_input_binding_section.Clear(); + s_input_binding_key.Clear(); + s_input_binding_display_name.Clear(); +} + +void BeginInputBinding(InputBindingType type, const std::string_view& section, const std::string_view& key, + const std::string_view& display_name) +{ + s_input_binding_type = type; + s_input_binding_section = section; + s_input_binding_key = key; + s_input_binding_display_name = display_name; + s_input_binding_timer.Reset(); + + ControllerInterface* ci = s_host_interface->GetControllerInterface(); + if (ci) + { + auto callback = [](const ControllerInterface::Hook& hook) -> ControllerInterface::Hook::CallbackResult { + // ignore if axis isn't at least halfway + if (hook.type == ControllerInterface::Hook::Type::Axis && std::abs(std::get(hook.value) > 0.5f)) + return ControllerInterface::Hook::CallbackResult::ContinueMonitoring; + + TinyString value; + switch (s_input_binding_type) + { + case InputBindingType::Axis: + { + if (hook.type == ControllerInterface::Hook::Type::Axis) + value.Format("Controller%d/Axis%d", hook.controller_index, hook.button_or_axis_number); + } + break; + + case InputBindingType::Button: + { + if (hook.type == ControllerInterface::Hook::Type::Axis) + value.Format("Controller%d/+Axis%d", hook.controller_index, hook.button_or_axis_number); + else if (hook.type == ControllerInterface::Hook::Type::Button) + value.Format("Controller%d/Button%d", hook.controller_index, hook.button_or_axis_number); + } + break; + + case InputBindingType::Rumble: + { + value.Format("Controller%d", hook.controller_index); + } + break; + + default: + break; + } + + if (value.IsEmpty()) + return ControllerInterface::Hook::CallbackResult::ContinueMonitoring; + + s_settings_interface->SetStringValue(s_input_binding_section, s_input_binding_key, value); + s_host_interface->AddFormattedOSDMessage(5.0f, "Set %s binding %s to %s.", s_input_binding_section.GetCharArray(), + s_input_binding_display_name.GetCharArray(), value.GetCharArray()); + + ClearInputBindingVariables(); + s_host_interface->RunLater(SaveAndApplySettings); + + return ControllerInterface::Hook::CallbackResult::StopMonitoring; + }; + ci->SetHook(std::move(callback)); + } +} + +void EndInputBinding() +{ + ClearInputBindingVariables(); + + ControllerInterface* ci = s_host_interface->GetControllerInterface(); + if (ci) + ci->ClearHook(); +} + +void DrawInputBindingWindow() +{ + DebugAssert(s_input_binding_type != InputBindingType::None); + + const double time_remaining = INPUT_BINDING_TIMEOUT_SECONDS - s_input_binding_timer.GetTimeSeconds(); + if (time_remaining <= 0.0) + { + EndInputBinding(); + return; + } + + const char* title = ICON_FA_GAMEPAD " Set Input Binding"; + ImGui::SetNextWindowSize(LayoutScale(500.0f, 0.0f)); + ImGui::SetNextWindowPos(ImGui::GetIO().DisplaySize * 0.5f, ImGuiCond_Always, ImVec2(0.5f, 0.5f)); + ImGui::OpenPopup(title); + + ImGui::PushFont(g_large_font); + ImGui::PushStyleVar(ImGuiStyleVar_WindowRounding, LayoutScale(10.0f)); + ImGui::PushStyleVar(ImGuiStyleVar_FramePadding, LayoutScale(ImGuiFullscreen::LAYOUT_MENU_BUTTON_X_PADDING, + ImGuiFullscreen::LAYOUT_MENU_BUTTON_Y_PADDING)); + ImGui::PushStyleVar(ImGuiStyleVar_WindowPadding, LayoutScale(10.0f, 10.0f)); + + if (ImGui::BeginPopupModal(title, nullptr, + ImGuiWindowFlags_NoResize | ImGuiWindowFlags_NoNav | ImGuiWindowFlags_NoInputs)) + { + ImGui::TextWrapped("Setting %s binding %s.", s_input_binding_section.GetCharArray(), + s_input_binding_display_name.GetCharArray()); + ImGui::TextUnformatted("Push a controller button or axis now."); + ImGui::NewLine(); + ImGui::Text("Timing out in %.0f seconds...", time_remaining); + ImGui::EndPopup(); + } + + ImGui::PopStyleVar(3); + ImGui::PopFont(); +} + +static bool SettingInfoButton(const SettingInfo& si, const char* section) +{ + // this.. isn't pretty :( + TinyString title; + title.Format("%s##%s/%s", si.visible_name, section, si.key); + switch (si.type) + { + case SettingInfo::Type::Boolean: + { + bool value = s_settings_interface->GetBoolValue(section, si.key, + StringUtil::FromChars(si.default_value).value_or(false)); + if (ToggleButton(title, si.description, &value)) + { + s_settings_interface->SetBoolValue(section, si.key, value); + return true; + } + + return false; + } + + case SettingInfo::Type::Integer: + { + int value = + s_settings_interface->GetIntValue(section, si.key, StringUtil::FromChars(si.default_value).value_or(0)); + const int min = StringUtil::FromChars(si.min_value).value_or(0); + const int max = StringUtil::FromChars(si.max_value).value_or(0); + const int step = StringUtil::FromChars(si.step_value).value_or(0); + if (RangeButton(title, si.description, &value, min, max, step)) + { + s_settings_interface->SetIntValue(section, si.key, value); + return true; + } + + return false; + } + + case SettingInfo::Type::Float: + { + float value = s_settings_interface->GetFloatValue(section, si.key, + StringUtil::FromChars(si.default_value).value_or(0)); + const float min = StringUtil::FromChars(si.min_value).value_or(0); + const float max = StringUtil::FromChars(si.max_value).value_or(0); + const float step = StringUtil::FromChars(si.step_value).value_or(0); + if (RangeButton(title, si.description, &value, min, max, step)) + { + s_settings_interface->SetFloatValue(section, si.key, value); + return true; + } + + return false; + } + + case SettingInfo::Type::Path: + { + std::string value = s_settings_interface->GetStringValue(section, si.key); + if (MenuButtonWithValue(title, si.description, value.c_str())) + { + std::string section_copy(section); + std::string key_copy(si.key); + auto callback = [section_copy, key_copy](const std::string& path) { + if (!path.empty()) + { + s_settings_interface->SetStringValue(section_copy.c_str(), key_copy.c_str(), path.c_str()); + s_host_interface->RunLater(SaveAndApplySettings); + } + + ClearImGuiFocus(); + CloseFileSelector(); + }; + OpenFileSelector(si.visible_name, false, std::move(callback), ImGuiFullscreen::FileSelectorFilters(), + FileSystem::GetPathDirectory(value.c_str()).c_str()); + } + + return false; + } + + default: + return false; + } +} + +static bool ToggleButtonForNonSetting(const char* title, const char* summary, const char* section, const char* key, + bool default_value, bool enabled = true, + float height = ImGuiFullscreen::LAYOUT_MENU_BUTTON_HEIGHT, + ImFont* font = g_large_font, ImFont* summary_font = g_medium_font) +{ + bool value = s_settings_interface->GetBoolValue(section, key, default_value); + if (!ToggleButton(title, summary, &value, enabled, height, font, summary_font)) + return false; + + s_settings_interface->SetBoolValue(section, key, value); + return true; +} + +void DrawSettingsWindow() +{ + BeginFullscreenColumns(); + + if (BeginFullscreenColumnWindow(0.0f, 300.0f, "settings_category", ImVec4(0.18f, 0.18f, 0.18f, 1.00f))) + { + static constexpr std::array(SettingsPage::Count)> titles = { + {ICON_FA_WINDOW_MAXIMIZE " Interface Settings", ICON_FA_LIST " Game List Settings", + ICON_FA_HDD " Console Settings", ICON_FA_SLIDERS_H " Emulation Settings", ICON_FA_MICROCHIP " BIOS Settings", + ICON_FA_GAMEPAD " Controller Settings", ICON_FA_KEYBOARD " Hotkey Settings", + ICON_FA_SD_CARD " Memory Card Settings", ICON_FA_TV " Display Settings", + ICON_FA_MAGIC " Enhancement Settings", ICON_FA_HEADPHONES " Audio Settings", + ICON_FA_EXCLAMATION_TRIANGLE " Advanced Settings"}}; + + BeginMenuButtons(); + for (u32 i = 0; i < static_cast(titles.size()); i++) + { + if (ActiveButton(titles[i], s_settings_page == static_cast(i))) + s_settings_page = static_cast(i); + } + + ImGui::SetCursorPosY(LayoutScale(670.0f)); + if (ActiveButton(ICON_FA_BACKWARD " Back", false)) + ReturnToMainWindow(); + + EndMenuButtons(); + } + + EndFullscreenColumnWindow(); + + if (BeginFullscreenColumnWindow(300.0f, LAYOUT_SCREEN_WIDTH, "settings_parent")) + { + bool settings_changed = false; + + switch (s_settings_page) + { + case SettingsPage::InterfaceSettings: + { + BeginMenuButtons(); + + MenuHeading("Behavior"); + + settings_changed |= + ToggleButton("Pause On Start", "Pauses the emulator when a game is started.", &s_settings_copy.start_paused); + settings_changed |= ToggleButton("Pause On Focus Loss", + "Pauses the emulator when you minimize the window or switch to another " + "application, and unpauses when you switch back.", + &s_settings_copy.pause_on_focus_loss); + settings_changed |= + ToggleButton("Confirm Power Off", + "Determines whether a prompt will be displayed to confirm shutting down the emulator/game " + "when the hotkey is pressed.", + &s_settings_copy.confim_power_off); + settings_changed |= + ToggleButton("Save State On Exit", + "Automatically saves the emulator state when powering down or exiting. You can then " + "resume directly from where you left off next time.", + &s_settings_copy.save_state_on_exit); + settings_changed |= + ToggleButton("Start Fullscreen", "Automatically switches to fullscreen mode when a game is started.", + &s_settings_copy.start_fullscreen); + settings_changed |= + ToggleButton("Load Devices From Save States", + "When enabled, memory cards and controllers will be overwritten when save states are loaded.", + &s_settings_copy.load_devices_from_save_states); + settings_changed |= ToggleButton( + "Apply Per-Game Settings", + "When enabled, per-game settings will be applied, and incompatible enhancements will be disabled.", + &s_settings_copy.apply_game_settings); + settings_changed |= + ToggleButton("Automatically Load Cheats", "Automatically loads and applies cheats on game start.", + &s_settings_copy.auto_load_cheats); + +#ifdef WITH_DISCORD_PRESENCE + MenuHeading("Integration"); + settings_changed |= ToggleButtonForNonSetting( + "Enable Discord Presence", "Shows the game you are currently playing as part of your profile on Discord.", + "Main", "EnableDiscordPresence", false); +#endif + + MenuHeading("Miscellaneous"); + + static ControllerInterface::Backend cbtype = ControllerInterface::Backend::None; + static bool cbtype_set = false; + if (!cbtype_set) + { + cbtype = ControllerInterface::ParseBackendName( + s_settings_interface->GetStringValue("Main", "ControllerBackend").c_str()) + .value_or(ControllerInterface::GetDefaultBackend()); + cbtype_set = true; + } + + if (EnumChoiceButton("Controller Backend", "Sets the API which is used to receive controller input.", &cbtype, + ControllerInterface::GetBackendName, ControllerInterface::Backend::Count)) + { + s_settings_interface->SetStringValue("Main", "ControllerBackend", + ControllerInterface::GetBackendName(cbtype)); + settings_changed = true; + } + + EndMenuButtons(); + } + break; + + case SettingsPage::GameListSettings: + { + EnsureGameListLoaded(); + + BeginMenuButtons(); + + MenuHeading("Game List"); + + if (MenuButton(ICON_FA_FOLDER_PLUS " Add Search Directory", "Adds a new directory to the game search list.")) + { + OpenFileSelector(ICON_FA_FOLDER_PLUS " Add Search Directory", true, [](const std::string& dir) { + if (!dir.empty()) + { + s_settings_interface->RemoveFromStringList("GameList", "RecursivePaths", dir.c_str()); + s_settings_interface->AddToStringList("GameList", "Paths", dir.c_str()); + } + + CloseFileSelector(); + }); + } + + if (MenuButton(ICON_FA_FOLDER_OPEN " Change Recursive Directories", + "Sets whether subdirectories are searched for each game directory")) + { + OpenChoiceDialog(ICON_FA_FOLDER_OPEN " Change Recursive Directories", true, + GetGameListDirectoryOptions(true), [](s32 index, const std::string& title, bool checked) { + if (index < 0) + return; + + if (checked) + { + s_settings_interface->RemoveFromStringList("GameList", "Paths", title.c_str()); + s_settings_interface->AddToStringList("GameList", "RecursivePaths", title.c_str()); + } + else + { + s_settings_interface->RemoveFromStringList("GameList", "RecursivePaths", title.c_str()); + s_settings_interface->AddToStringList("GameList", "Paths", title.c_str()); + } + + s_host_interface->RunLater(SaveAndApplySettings); + }); + } + + if (MenuButton(ICON_FA_FOLDER_MINUS " Remove Search Directory", + "Removes a directory from the game search list.")) + { + OpenChoiceDialog(ICON_FA_FOLDER_MINUS " Remove Search Directory", false, GetGameListDirectoryOptions(false), + [](s32 index, const std::string& title, bool checked) { + if (index < 0) + return; + + s_settings_interface->RemoveFromStringList("GameList", "Paths", title.c_str()); + s_settings_interface->RemoveFromStringList("GameList", "RecursivePaths", title.c_str()); + s_host_interface->RunLater(SaveAndApplySettings); + CloseChoiceDialog(); + }); + } + + MenuHeading("Search Directories"); + for (const GameList::DirectoryEntry& entry : s_host_interface->GetGameList()->GetSearchDirectories()) + ActiveButton(entry.path.c_str(), false, false); + + EndMenuButtons(); + } + break; + + case SettingsPage::ConsoleSettings: + { + static constexpr auto cdrom_read_speeds = + make_array("None (Double Speed)", "2x (Quad Speed)", "3x (6x Speed)", "4x (8x Speed)", "5x (10x Speed)", + "6x (12x Speed)", "7x (14x Speed)", "8x (16x Speed)", "9x (18x Speed)", "10x (20x Speed)"); + + BeginMenuButtons(); + + MenuHeading("Console Settings"); + + settings_changed |= + EnumChoiceButton("Region", "Determines the emulated hardware type.", &s_settings_copy.region, + &Settings::GetConsoleRegionDisplayName, ConsoleRegion::Count); + + MenuHeading("CPU Emulation (MIPS R3000A Derivative)"); + + settings_changed |= EnumChoiceButton( + "Execution Mode", "Determines how the emulated CPU executes instructions. Recompiler is recommended.", + &s_settings_copy.cpu_execution_mode, &Settings::GetCPUExecutionModeDisplayName, CPUExecutionMode::Count); + + settings_changed |= + ToggleButton("Enable Overclocking", "When this option is chosen, the clock speed set below will be used.", + &s_settings_copy.cpu_overclock_enable); + + s32 overclock_percent = + s_settings_copy.cpu_overclock_enable ? static_cast(s_settings_copy.GetCPUOverclockPercent()) : 100; + if (RangeButton("Overclocking Percentage", + "Selects the percentage of the normal clock speed the emulated hardware will run at.", + &overclock_percent, 10, 1000, 10, "%d%%", s_settings_copy.cpu_overclock_enable)) + { + s_settings_copy.SetCPUOverclockPercent(static_cast(overclock_percent)); + settings_changed = true; + } + + MenuHeading("CD-ROM Emulation"); + + const u32 read_speed_index = + std::min(g_settings.cdrom_read_speedup, static_cast(cdrom_read_speeds.size() + 1)) - 1; + if (MenuButtonWithValue("Read Speedup", + "Speeds up CD-ROM reads by the specified factor. May improve loading speeds in some " + "games, and break others.", + cdrom_read_speeds[read_speed_index])) + { + ImGuiFullscreen::ChoiceDialogOptions options; + options.reserve(cdrom_read_speeds.size()); + for (u32 i = 0; i < static_cast(cdrom_read_speeds.size()); i++) + options.emplace_back(cdrom_read_speeds[i], i == read_speed_index); + OpenChoiceDialog("CD-ROM Read Speedup", false, std::move(options), + [](s32 index, const std::string& title, bool checked) { + if (index >= 0) + s_settings_copy.cdrom_read_speedup = static_cast(index) + 1; + CloseChoiceDialog(); + }); + } + + settings_changed |= ToggleButton( + "Enable Read Thread", + "Reduces hitches in emulation by reading/decompressing CD data asynchronously on a worker thread.", + &s_settings_copy.cdrom_read_thread); + settings_changed |= + ToggleButton("Enable Region Check", "Simulates the region check present in original, unmodified consoles.", + &s_settings_copy.cdrom_region_check); + settings_changed |= ToggleButton( + "Preload Images to RAM", + "Loads the game image into RAM. Useful for network paths that may become unreliable during gameplay.", + &s_settings_copy.cdrom_load_image_to_ram); + + EndMenuButtons(); + } + break; + + case SettingsPage::EmulationSettings: + { + static constexpr auto emulation_speeds = + make_array(0.0f, 0.1f, 0.2f, 0.3f, 0.4f, 0.5f, 0.6f, 0.7f, 0.8f, 0.9f, 1.0f, 1.25f, 1.5f, 1.75f, 2.0f, 2.5f, + 3.0f, 3.5f, 4.0f, 4.5f, 5.0f, 6.0f, 7.0f, 8.0f, 9.0f, 10.0f); + static constexpr auto get_emulation_speed_options = [](float current_speed) { + ImGuiFullscreen::ChoiceDialogOptions options; + options.reserve(emulation_speeds.size()); + for (const float speed : emulation_speeds) + { + options.emplace_back( + (speed != 0.0f) ? + StringUtil::StdStringFromFormat("%d%% [%d FPS (NTSC) / %d FPS (PAL)]", static_cast(speed * 100.0f), + static_cast(60.0f * speed), static_cast(50.0f * speed)) : + "Unlimited", + speed == current_speed); + } + return options; + }; + + BeginMenuButtons(); + + MenuHeading("Speed Control"); + +#define MAKE_EMULATION_SPEED(setting_title, setting_var) \ + if (MenuButtonWithValue( \ + setting_title, \ + "Sets the target emulation speed. It is not guaranteed that this speed will be reached on all systems.", \ + (setting_var != 0.0f) ? TinyString::FromFormat("%.0f%%", setting_var * 100.0f) : TinyString("Unlimited"))) \ + { \ + OpenChoiceDialog(setting_title, false, get_emulation_speed_options(setting_var), \ + [](s32 index, const std::string& title, bool checked) { \ + if (index >= 0) \ + { \ + setting_var = emulation_speeds[index]; \ + s_host_interface->RunLater(SaveAndApplySettings); \ + } \ + CloseChoiceDialog(); \ + }); \ + } + + MAKE_EMULATION_SPEED("Emulation Speed", s_settings_copy.emulation_speed); + MAKE_EMULATION_SPEED("Fast Forward Speed", s_settings_copy.fast_forward_speed); + MAKE_EMULATION_SPEED("Turbo Speed", s_settings_copy.turbo_speed); + +#undef MAKE_EMULATION_SPEED + + settings_changed |= ToggleButton("Sync To Host Refresh Rate", + "Adjusts the emulation speed so the console's refresh rate matches the host " + "when VSync and Audio Resampling are enabled.", + &s_settings_copy.sync_to_host_refresh_rate, + s_settings_copy.video_sync_enabled && s_settings_copy.audio_resampling); + + MenuHeading("Runahead/Rewind"); + + settings_changed |= + ToggleButton("Enable Rewinding", "Saves state periodically so you can rewind any mistakes while playing.", + &s_settings_copy.rewind_enable); + settings_changed |= RangeButton( + "Rewind Save Frequency", + "How often a rewind state will be created. Higher frequencies have greater system requirements.", + &s_settings_copy.rewind_save_frequency, 0.0f, 3600.0f, 0.1f, "%.2f Seconds", s_settings_copy.rewind_enable); + settings_changed |= + RangeButton("Rewind Save Frequency", + "How many saves will be kept for rewinding. Higher values have greater memory requirements.", + reinterpret_cast(&s_settings_copy.rewind_save_slots), 1, 10000, 1, "%d Frames", + s_settings_copy.rewind_enable); + + TinyString summary; + if (!s_settings_copy.IsRunaheadEnabled()) + summary = "Disabled"; + else + summary.Format("%u Frames", s_settings_copy.runahead_frames); + + if (MenuButtonWithValue("Runahead", + "Simulates the system ahead of time and rolls back/replays to reduce input lag. Very " + "high system requirements.", + summary)) + { + ImGuiFullscreen::ChoiceDialogOptions options; + for (u32 i = 0; i <= 10; i++) + { + if (i == 0) + options.emplace_back("Disabled", s_settings_copy.runahead_frames == i); + else + options.emplace_back(StringUtil::StdStringFromFormat("%u Frames", i), + s_settings_copy.runahead_frames == i); + } + OpenChoiceDialog("Runahead", false, std::move(options), + [](s32 index, const std::string& title, bool checked) { + s_settings_copy.runahead_frames = index; + s_host_interface->RunLater(SaveAndApplySettings); + CloseChoiceDialog(); + }); + settings_changed = true; + } + + TinyString rewind_summary; + if (s_settings_copy.IsRunaheadEnabled()) + { + rewind_summary = "Rewind is disabled because runahead is enabled. Runahead will significantly increase " + "system requirements."; + } + else if (s_settings_copy.rewind_enable) + { + const float duration = ((s_settings_copy.rewind_save_frequency <= std::numeric_limits::epsilon()) ? + (1.0f / 60.0f) : + s_settings_copy.rewind_save_frequency) * + static_cast(s_settings_copy.rewind_save_slots); + + u64 ram_usage, vram_usage; + System::CalculateRewindMemoryUsage(s_settings_copy.rewind_save_slots, &ram_usage, &vram_usage); + rewind_summary.Format("Rewind for %u frames, lasting %.2f seconds will require up to %" PRIu64 + "MB of RAM and %" PRIu64 "MB of VRAM.", + s_settings_copy.rewind_save_slots, duration, ram_usage / 1048576, vram_usage / 1048576); + } + else + { + rewind_summary = + "Rewind is not enabled. Please note that enabling rewind may significantly increase system requirements."; + } + + ActiveButton(rewind_summary, false, false, ImGuiFullscreen::LAYOUT_MENU_BUTTON_HEIGHT_NO_SUMMARY, + g_medium_font); + + EndMenuButtons(); + } + break; + + case SettingsPage::BIOSSettings: + { + static constexpr auto config_keys = make_array("", "PathNTSCJ", "PathNTSCU", "PathPAL"); + static std::string bios_region_filenames[static_cast(ConsoleRegion::Count)]; + static std::string bios_directory; + static bool bios_filenames_loaded = false; + + if (!bios_filenames_loaded) + { + for (u32 i = 0; i < static_cast(ConsoleRegion::Count); i++) + { + if (i == static_cast(ConsoleRegion::Auto)) + continue; + bios_region_filenames[i] = s_settings_interface->GetStringValue("BIOS", config_keys[i]); + } + bios_directory = s_host_interface->GetBIOSDirectory(); + bios_filenames_loaded = true; + } + + BeginMenuButtons(); + + MenuHeading("BIOS Selection"); + + for (u32 i = 0; i < static_cast(ConsoleRegion::Count); i++) + { + const ConsoleRegion region = static_cast(i); + if (region == ConsoleRegion::Auto) + continue; + + TinyString title; + title.Format("BIOS for %s", Settings::GetConsoleRegionName(region)); + + if (MenuButtonWithValue(title, + SmallString::FromFormat("BIOS to use when emulating %s consoles.", + Settings::GetConsoleRegionDisplayName(region)), + bios_region_filenames[i].c_str())) + { + ImGuiFullscreen::ChoiceDialogOptions options; + auto images = s_host_interface->FindBIOSImagesInDirectory(s_host_interface->GetBIOSDirectory().c_str()); + options.reserve(images.size() + 1); + options.emplace_back("Auto-Detect", bios_region_filenames[i].empty()); + for (auto& [path, info] : images) + { + const bool selected = bios_region_filenames[i] == path; + options.emplace_back(std::move(path), selected); + } + + OpenChoiceDialog(title, false, std::move(options), [i](s32 index, const std::string& path, bool checked) { + if (index >= 0) + { + bios_region_filenames[i] = path; + s_settings_interface->SetStringValue("BIOS", config_keys[i], path.c_str()); + s_settings_interface->Save(); + } + CloseChoiceDialog(); + }); + } + } + + if (MenuButton("BIOS Directory", bios_directory.c_str())) + { + OpenFileSelector("BIOS Directory", true, [](const std::string& path) { + if (!path.empty()) + { + bios_directory = path; + s_settings_interface->SetStringValue("BIOS", "SearchDirectory", path.c_str()); + s_settings_interface->Save(); + } + CloseFileSelector(); + }); + } + + MenuHeading("Patches"); + + settings_changed |= + ToggleButton("Enable Fast Boot", "Patches the BIOS to skip the boot animation. Safe to enable.", + &s_settings_copy.bios_patch_fast_boot); + settings_changed |= ToggleButton( + "Enable TTY Output", "Patches the BIOS to log calls to printf(). Only use when debugging, can break games.", + &s_settings_copy.bios_patch_tty_enable); + + EndMenuButtons(); + } + break; + + case SettingsPage::ControllerSettings: + { + BeginMenuButtons(); + + MenuHeading("Input Profiles"); + if (MenuButton(ICON_FA_FOLDER_OPEN " Load Input Profile", + "Applies a saved configuration of controller types and bindings.")) + { + CommonHostInterface::InputProfileList profiles(s_host_interface->GetInputProfileList()); + ImGuiFullscreen::ChoiceDialogOptions options; + options.reserve(profiles.size()); + for (const CommonHostInterface::InputProfileEntry& entry : profiles) + options.emplace_back(std::move(entry.name), false); + + auto callback = [profiles](s32 index, const std::string& title, bool checked) { + if (index < 0) + return; + + // needs a reload... + s_host_interface->ApplyInputProfile(profiles[index].path.c_str(), *s_settings_interface); + s_settings_copy.Load(*s_settings_interface); + s_host_interface->RunLater(SaveAndApplySettings); + CloseChoiceDialog(); + }; + OpenChoiceDialog(ICON_FA_FOLDER_OPEN " Load Input Profile", false, std::move(options), std::move(callback)); + } + + static std::array type_cache = {}; + static std::array button_cache; + static std::array axis_cache; + static std::array setting_cache; + TinyString section; + TinyString key; + + for (u32 port = 0; port < NUM_CONTROLLER_AND_CARD_PORTS; port++) + { + MenuHeading(TinyString::FromFormat("Controller Port %u", port + 1)); + settings_changed |= EnumChoiceButton( + TinyString::FromFormat(ICON_FA_GAMEPAD " Controller Type##type%u", port), + "Determines the simulated controller plugged into this port.", &s_settings_copy.controller_types[port], + &Settings::GetControllerTypeDisplayName, ControllerType::Count); + + const ControllerType ctype = s_settings_copy.controller_types[port]; + if (ctype != type_cache[port]) + { + button_cache[port] = Controller::GetButtonNames(ctype); + axis_cache[port] = Controller::GetAxisNames(ctype); + setting_cache[port] = Controller::GetSettings(ctype); + } + + section.Format("Controller%u", port + 1); + + for (const auto& it : button_cache[port]) + { + key.Format("Button%s", it.first.c_str()); + DrawInputBindingButton(InputBindingType::Button, section, key, it.first.c_str()); + } + + for (const auto& it : axis_cache[port]) + { + key.Format("Axis%s", std::get<0>(it).c_str()); + DrawInputBindingButton(InputBindingType::Axis, section, key, std::get<0>(it).c_str()); + } + + if (Controller::GetVibrationMotorCount(ctype) > 0) + DrawInputBindingButton(InputBindingType::Rumble, section, "Rumble", "Rumble/Vibration"); + + for (const SettingInfo& it : setting_cache[port]) + settings_changed |= SettingInfoButton(it, section); + } + + EndMenuButtons(); + } + break; + + case SettingsPage::HotkeySettings: + { + BeginMenuButtons(); + + TinyString last_category; + for (const CommonHostInterface::HotkeyInfo& hotkey : s_host_interface->GetHotkeyInfoList()) + { + if (hotkey.category != last_category) + { + MenuHeading(hotkey.category); + last_category = hotkey.category; + } + + DrawInputBindingButton(InputBindingType::Button, "Hotkeys", hotkey.name, hotkey.display_name); + } + + EndMenuButtons(); + } + break; + + case SettingsPage::MemoryCardSettings: + { + BeginMenuButtons(); + + for (u32 i = 0; i < 2; i++) + { + MenuHeading(TinyString::FromFormat("Memory Card Port %u", i + 1)); + + settings_changed |= EnumChoiceButton( + TinyString::FromFormat("Memory Card %u Type", i + 1), + SmallString::FromFormat("Sets which sort of memory card image will be used for slot %u.", i + 1), + &s_settings_copy.memory_card_types[i], &Settings::GetMemoryCardTypeDisplayName, MemoryCardType::Count); + + settings_changed |= MenuButton(TinyString::FromFormat("Shared Memory Card %u Path", i + 1), + s_settings_copy.memory_card_paths[i].c_str(), + s_settings_copy.memory_card_types[i] == MemoryCardType::Shared); + } + + MenuHeading("Shared Settings"); + + settings_changed |= ToggleButton( + "Use Single Card For Playlist", + "When using a playlist (m3u) and per-game (title) memory cards, use a single memory card for all discs.", + &s_settings_copy.memory_card_use_playlist_title); + + static std::string memory_card_directory; + if (memory_card_directory.empty()) + memory_card_directory = s_host_interface->GetUserDirectoryRelativePath("memcards"); + + MenuButton("Per-Game Memory Card Directory", memory_card_directory.c_str(), false); + + EndMenuButtons(); + } + break; + + case SettingsPage::DisplaySettings: + { + BeginMenuButtons(); + + MenuHeading("Device Settings"); + + settings_changed |= + EnumChoiceButton("GPU Renderer", "Chooses the backend to use for rendering the console/game visuals.", + &s_settings_copy.gpu_renderer, &Settings::GetRendererDisplayName, GPURenderer::Count); + + settings_changed |= + ToggleButton("Enable VSync", + "Synchronizes presentation of the console's frames to the host. Enable for smoother animations.", + &s_settings_copy.video_sync_enabled); + + switch (s_settings_copy.gpu_renderer) + { +#ifdef WIN32 + case GPURenderer::HardwareD3D11: + { + // TODO: FIXME + bool use_blit_swap_chain = false; + settings_changed |= ToggleButtonForNonSetting( + "Use Blit Swap Chain", + "Uses a blit presentation model instead of flipping. This may be needed on some systems.", "Display", + "UseBlitSwapChain", false); + } + break; +#endif + + case GPURenderer::HardwareVulkan: + { + settings_changed |= + ToggleButton("Threaded Presentation", + "Presents frames on a background thread when fast forwarding or vsync is disabled.", + &s_settings_copy.gpu_threaded_presentation); + } + break; + + case GPURenderer::Software: + { + settings_changed |= ToggleButton("Threaded Rendering", + "Uses a second thread for drawing graphics. Speed boost, and safe to use.", + &s_settings_copy.gpu_use_thread); + } + break; + + default: + break; + } + + settings_changed |= ToggleButton("Optimal Frame Pacing", + "Ensures every frame generated is displayed for optimal pacing. Disable if " + "you are having speed or sound issues.", + &s_settings_copy.display_all_frames); + + MenuHeading("Screen Display"); + + settings_changed |= EnumChoiceButton( + "Aspect Ratio", "Changes the aspect ratio used to display the console's output to the screen.", + &s_settings_copy.display_aspect_ratio, &Settings::GetDisplayAspectRatioName, DisplayAspectRatio::Count); + + settings_changed |= EnumChoiceButton( + "Crop Mode", "Determines how much of the area typically not visible on a consumer TV set to crop/hide.", + &s_settings_copy.display_crop_mode, &Settings::GetDisplayCropModeDisplayName, DisplayCropMode::Count); + + settings_changed |= + EnumChoiceButton("Downsampling", + "Downsamples the rendered image prior to displaying it. Can improve " + "overall image quality in mixed 2D/3D games.", + &s_settings_copy.gpu_downsample_mode, &Settings::GetDownsampleModeDisplayName, + GPUDownsampleMode::Count, !s_settings_copy.IsUsingSoftwareRenderer()); + + settings_changed |= + ToggleButton("Linear Upscaling", "Uses a bilinear filter when upscaling to display, smoothing out the image.", + &s_settings_copy.display_linear_filtering); + + settings_changed |= + ToggleButton("Integer Upscaling", "Adds padding to ensure pixels are a whole number in size.", + &s_settings_copy.display_integer_scaling); + + MenuHeading("On-Screen Display"); + + settings_changed |= ToggleButton("Show OSD Messages", "Shows on-screen-display messages when events occur.", + &s_settings_copy.display_show_osd_messages); + settings_changed |= ToggleButton( + "Show Game FPS", "Shows the internal frame rate of the game in the top-right corner of the display.", + &s_settings_copy.display_show_fps); + settings_changed |= ToggleButton("Show Display FPS (VPS)", + "Shows the number of frames (or v-syncs) displayed per second by the system " + "in the top-right corner of the display.", + &s_settings_copy.display_show_vps); + settings_changed |= ToggleButton( + "Show Speed", + "Shows the current emulation speed of the system in the top-right corner of the display as a percentage.", + &s_settings_copy.display_show_speed); + settings_changed |= + ToggleButton("Show Resolution", + "Shows the current rendering resolution of the system in the top-right corner of the display.", + &s_settings_copy.display_show_resolution); + + EndMenuButtons(); + } + break; + + case SettingsPage::EnhancementSettings: + { + static const auto resolution_scale_text_callback = [](u32 value) -> const char* { + static constexpr std::array texts = { + {"Automatic based on window size", "1x", "2x", "3x (for 720p)", "4x", "5x (for 1080p)", "6x (for 1440p)", + "7x", "8x", "9x (for 4K)", "10x", "11x", "12x", "13x", "14x", "15x", "16x" + + }}; + return (value >= texts.size()) ? "" : texts[value]; + }; + + BeginMenuButtons(); + + MenuHeading("Rendering Enhancements"); + + settings_changed |= EnumChoiceButton( + "Internal Resolution Scale", + "Scales internal VRAM resolution by the specified multiplier. Some games require 1x VRAM resolution.", + &s_settings_copy.gpu_resolution_scale, resolution_scale_text_callback, 17); + settings_changed |= EnumChoiceButton( + "Texture Filtering", + "Smooths out the blockyness of magnified textures on 3D objects. Will have a greater effect " + "on higher resolution scales.", + &s_settings_copy.gpu_texture_filter, &Settings::GetTextureFilterDisplayName, GPUTextureFilter::Count); + settings_changed |= + ToggleButton("True Color Rendering", + "Disables dithering and uses the full 8 bits per channel of color information. May break " + "rendering in some games.", + &s_settings_copy.gpu_true_color); + settings_changed |= ToggleButton( + "Scaled Dithering", + "Scales the dithering pattern with the internal rendering resolution, making it less noticeable. " + "Usually safe to enable.", + &s_settings_copy.gpu_scaled_dithering, s_settings_copy.gpu_resolution_scale > 1); + settings_changed |= ToggleButton( + "Widescreen Hack", "Increases the field of view from 4:3 to the chosen display aspect ratio in 3D games.", + &s_settings_copy.gpu_widescreen_hack); + + MenuHeading("Display Enhancements"); + + settings_changed |= + ToggleButton("Disable Interlacing", + "Disables interlaced rendering and display in the GPU. Some games can render in 480p this way, " + "but others will break.", + &s_settings_copy.gpu_disable_interlacing); + settings_changed |= ToggleButton( + "Force NTSC Timings", + "Forces PAL games to run at NTSC timings, i.e. 60hz. Some PAL games will run at their \"normal\" " + "speeds, while others will break.", + &s_settings_copy.gpu_force_ntsc_timings); + settings_changed |= + ToggleButton("Force 4:3 For 24-Bit Display", + "Switches back to 4:3 display aspect ratio when displaying 24-bit content, usually FMVs.", + &s_settings_copy.display_force_4_3_for_24bit); + settings_changed |= ToggleButton( + "Chroma Smoothing For 24-Bit Display", + "Smooths out blockyness between colour transitions in 24-bit content, usually FMVs. Only applies " + "to the hardware renderers.", + &s_settings_copy.gpu_24bit_chroma_smoothing); + + MenuHeading("PGXP (Precision Geometry Transform Pipeline"); + + settings_changed |= + ToggleButton("PGXP Geometry Correction", + "Reduces \"wobbly\" polygons by attempting to preserve the fractional component through memory " + "transfers.", + &s_settings_copy.gpu_pgxp_enable); + settings_changed |= + ToggleButton("PGXP Texture Correction", + "Uses perspective-correct interpolation for texture coordinates and colors, straightening out " + "warped textures.", + &s_settings_copy.gpu_pgxp_texture_correction, s_settings_copy.gpu_pgxp_enable); + settings_changed |= + ToggleButton("PGXP Culling Correction", + "Increases the precision of polygon culling, reducing the number of holes in geometry.", + &s_settings_copy.gpu_pgxp_culling, s_settings_copy.gpu_pgxp_enable); + settings_changed |= ToggleButton( + "PGXP Depth Buffer", "Reduces polygon Z-fighting through depth testing. Low compatibility with games.", + &s_settings_copy.gpu_pgxp_depth_buffer, + s_settings_copy.gpu_pgxp_enable && s_settings_copy.gpu_pgxp_texture_correction); + + EndMenuButtons(); + } + break; + + case SettingsPage::AudioSettings: + { + BeginMenuButtons(); + + MenuHeading("Audio Control"); + + settings_changed |= RangeButton("Output Volume", "Controls the volume of the audio played on the host.", + &s_settings_copy.audio_output_volume, 0, 100, 1, "%d%%"); + settings_changed |= RangeButton("Fast Forward Volume", + "Controls the volume of the audio played on the host when fast forwarding.", + &s_settings_copy.audio_output_volume, 0, 100, 1, "%d%%"); + settings_changed |= ToggleButton("Mute All Sound", "Prevents the emulator from producing any audible sound.", + &s_settings_copy.audio_output_muted); + settings_changed |= ToggleButton("Mute CD Audio", + "Forcibly mutes both CD-DA and XA audio from the CD-ROM. Can be used to " + "disable background music in some games.", + &s_settings_copy.cdrom_mute_cd_audio); + + MenuHeading("Backend Settings"); + + settings_changed |= EnumChoiceButton( + "Audio Backend", + "The audio backend determines how frames produced by the emulator are submitted to the host.", + &s_settings_copy.audio_backend, &Settings::GetAudioBackendDisplayName, AudioBackend::Count); + settings_changed |= RangeButton( + "Buffer Size", "The buffer size determines the size of the chunks of audio which will be pulled by the host.", + reinterpret_cast(&s_settings_copy.audio_buffer_size), 1024, 8192, 128, "%d Frames"); + + settings_changed |= ToggleButton("Sync To Output", + "Throttles the emulation speed based on the audio backend pulling audio " + "frames. Enable to reduce the chances of crackling.", + &s_settings_copy.audio_sync_enabled); + settings_changed |= ToggleButton( + "Resampling", + "When running outside of 100% speed, resamples audio from the target speed instead of dropping frames.", + &s_settings_copy.audio_resampling); + + EndMenuButtons(); + } + break; + + case SettingsPage::AdvancedSettings: + { + BeginMenuButtons(); + + MenuHeading("Logging Settings"); + settings_changed |= + EnumChoiceButton("Log Level", "Sets the verbosity of messages logged. Higher levels will log more messages.", + &g_settings.log_level, &Settings::GetLogLevelDisplayName, LOGLEVEL_COUNT); + settings_changed |= + ToggleButton("Log To System Console", "Logs messages to the console window.", &g_settings.log_to_console); + settings_changed |= ToggleButton("Log To Debug Console", "Logs messages to the debug console where supported.", + &g_settings.log_to_debug); + settings_changed |= ToggleButton("Log To File", "Logs messages to duckstation.log in the user directory.", + &g_settings.log_to_file); + + MenuHeading("Debugging Settings"); + + bool debug_menu = s_debug_menu_enabled; + if (ToggleButton("Enable Debug Menu", "Shows a debug menu bar with additional statistics and quick settings.", + &debug_menu)) + { + s_host_interface->RunLater([debug_menu]() { SetDebugMenuEnabled(debug_menu, true); }); + } + + settings_changed |= + ToggleButton("Disable All Enhancements", "Temporarily disables all enhancements, useful when testing.", + &s_settings_copy.disable_all_enhancements); + + settings_changed |= ToggleButton( + "Use Debug GPU Device", "Enable debugging when supported by the host's renderer API. Only for developer use.", + &s_settings_copy.gpu_use_debug_device); + +#ifdef WIN32 + settings_changed |= + ToggleButton("Increase Timer Resolution", "Enables more precise frame pacing at the cost of battery life.", + &s_settings_copy.increase_timer_resolution); +#endif + + MenuHeading("Display Settings"); + settings_changed |= RangeButton( + "Display FPS Limit", "Limits how many frames are displayed to the screen. These frames are still rendered.", + &s_settings_copy.display_max_fps, 0.0f, 500.0f, 1.0f, "%.2f FPS"); + + MenuHeading("PGXP Settings"); + + settings_changed |= + ToggleButton("Enable PGXP CPU Mode", "Uses PGXP for all instructions, not just memory operations.", + &s_settings_copy.gpu_pgxp_cpu, s_settings_copy.gpu_pgxp_enable); + settings_changed |= ToggleButton( + "Enable PGXP Vertex Cache", "Uses screen positions to resolve PGXP data. May improve visuals in some games.", + &s_settings_copy.gpu_pgxp_vertex_cache, s_settings_copy.gpu_pgxp_enable); + settings_changed |= + ToggleButton("Enable PGXP Preserve Projection Precision", + "Adds additional precision to PGXP data post-projection. May improve visuals in some games.", + &s_settings_copy.gpu_pgxp_preserve_proj_fp, s_settings_copy.gpu_pgxp_enable); + settings_changed |= RangeButton( + "PGXP Geometry Tolerance", + "Sets a threshold for discarding precise values when exceeded. May help with glitches in some games.", + &s_settings_copy.gpu_pgxp_tolerance, -1.0f, 10.0f, 0.1f, "%.1f Pixels", s_settings_copy.gpu_pgxp_enable); + settings_changed |= RangeButton( + "PGXP Depth Clear Threshold", + "Sets a threshold for discarding the emulated depth buffer. May help in some games.", + &s_settings_copy.gpu_pgxp_tolerance, 0.0f, 4096.0f, 1.0f, "%.1f", s_settings_copy.gpu_pgxp_enable); + + MenuHeading("Texture Dumping/Replacements"); + + settings_changed |= ToggleButton("Enable VRAM Write Texture Replacement", + "Enables the replacement of background textures in supported games.", + &s_settings_copy.texture_replacements.enable_vram_write_replacements); + settings_changed |= ToggleButton("Preload Replacement Textures", + "Loads all replacement texture to RAM, reducing stuttering at runtime.", + &s_settings_copy.texture_replacements.preload_textures, + s_settings_copy.texture_replacements.AnyReplacementsEnabled()); + settings_changed |= + ToggleButton("Dump Replacable VRAM Writes", "Writes textures which can be replaced to the dump directory.", + &s_settings_copy.texture_replacements.dump_vram_writes); + settings_changed |= + ToggleButton("Set VRAM Write Dump Alpha Channel", "Clears the mask/transparency bit in VRAM write dumps.", + &s_settings_copy.texture_replacements.dump_vram_write_force_alpha_channel, + s_settings_copy.texture_replacements.dump_vram_writes); + + MenuHeading("CPU Emulation"); + + settings_changed |= + ToggleButton("Enable Recompiler ICache", + "Simulates the CPU's instruction cache in the recompiler. Can help with games running too fast.", + &s_settings_copy.cpu_recompiler_icache); + settings_changed |= ToggleButton("Enable Recompiler Memory Exceptions", + "Enables alignment and bus exceptions. Not needed for any known games.", + &s_settings_copy.cpu_recompiler_memory_exceptions); + settings_changed |= EnumChoiceButton("Recompiler Fast Memory Access", + "Avoids calls to C++ code, significantly speeding up the recompiler.", + &s_settings_copy.cpu_fastmem_mode, &Settings::GetCPUFastmemModeDisplayName, + CPUFastmemMode::Count, !s_settings_copy.cpu_recompiler_memory_exceptions); + + EndMenuButtons(); + } + break; + } + + if (settings_changed) + s_host_interface->RunLater(SaveAndApplySettings); + } + + EndFullscreenColumnWindow(); + + EndFullscreenColumns(); +} + +void DrawQuickMenu(MainWindowType type) +{ + ImDrawList* dl = ImGui::GetBackgroundDrawList(); + dl->AddRectFilled(ImVec2(0.0f, 0.0f), ImGui::GetIO().DisplaySize, IM_COL32(0x21, 0x21, 0x21, 200)); + + // title info + { + const std::string& title = System::GetRunningTitle(); + const std::string& code = System::GetRunningCode(); + + SmallString subtitle; + if (!code.empty()) + subtitle.Format("%s - ", code.c_str()); + subtitle.AppendString(FileSystem::GetFileNameFromPath(System::GetRunningPath().c_str())); + + const ImVec2 title_size( + g_large_font->CalcTextSizeA(g_large_font->FontSize, std::numeric_limits::max(), -1.0f, title.c_str())); + const ImVec2 subtitle_size( + g_medium_font->CalcTextSizeA(g_medium_font->FontSize, std::numeric_limits::max(), -1.0f, subtitle)); + + const ImVec2 title_pos(LayoutScale(LAYOUT_SCREEN_WIDTH - 20.0f - 50.0f - 20.0f) - title_size.x, + LayoutScale(LAYOUT_SCREEN_HEIGHT - 20.0f - 50.0f)); + const ImVec2 subtitle_pos(LayoutScale(LAYOUT_SCREEN_WIDTH - 20.0f - 50.0f - 20.0f) - subtitle_size.x, + title_pos.y + g_large_font->FontSize + LayoutScale(4.0f)); + + dl->AddText(g_large_font, g_large_font->FontSize, title_pos, IM_COL32(255, 255, 255, 255), title.c_str()); + dl->AddText(g_medium_font, g_medium_font->FontSize, subtitle_pos, IM_COL32(255, 255, 255, 255), subtitle); + + const ImVec2 image_min(LayoutScale(LAYOUT_SCREEN_WIDTH - 20.0f - 50.0f, LAYOUT_SCREEN_HEIGHT - 20.0f - 50.0f)); + const ImVec2 image_max(image_min + LayoutScale(50.0f, 50.0f)); + dl->AddImage(GetCoverForCurrentGame()->GetHandle(), image_min, image_max); + } + + if (BeginFullscreenWindow(0.0f, 0.0f, 500.0f, LAYOUT_SCREEN_HEIGHT, "pause_menu", ImVec4(0.0f, 0.0f, 0.0f, 0.0f), + 0.0f, 10.0f, ImGuiWindowFlags_NoBackground)) + { + BeginMenuButtons(11, 1.0f, ImGuiFullscreen::LAYOUT_MENU_BUTTON_X_PADDING, + ImGuiFullscreen::LAYOUT_MENU_BUTTON_Y_PADDING, + ImGuiFullscreen::LAYOUT_MENU_BUTTON_HEIGHT_NO_SUMMARY); + + if (ActiveButton(ICON_FA_PLAY " Resume Game", false)) + CloseQuickMenu(); + + if (ActiveButton(ICON_FA_FAST_FORWARD " Fast Forward", false)) + { + s_host_interface->RunLater( + []() { s_host_interface->SetFastForwardEnabled(!s_host_interface->IsFastForwardEnabled()); }); + CloseQuickMenu(); + } + + if (ActiveButton(ICON_FA_CAMERA " Save Screenshot", false)) + { + CloseQuickMenu(); + s_host_interface->RunLater([]() { s_host_interface->SaveScreenshot(); }); + } + + if (ActiveButton(ICON_FA_UNDO " Load State", false)) + { + OpenSaveStateSelector(true); + CloseQuickMenu(); + } + + if (ActiveButton(ICON_FA_SAVE " Save State", false)) + { + OpenSaveStateSelector(false); + CloseQuickMenu(); + } + + if (ActiveButton(ICON_FA_FROWN_OPEN " Cheat List", false)) + { + CloseQuickMenu(); + DoCheatsMenu(); + } + + if (ActiveButton(ICON_FA_GAMEPAD " Toggle Analog", false)) + { + CloseQuickMenu(); + DoToggleAnalogMode(); + } + + if (ActiveButton(ICON_FA_COMPACT_DISC " Change Disc", false)) + { + CloseQuickMenu(); + DoChangeDisc(); + } + + if (ActiveButton(ICON_FA_SLIDERS_H " Settings", false)) + { + CloseQuickMenu(); + s_current_main_window = MainWindowType::Settings; + } + + if (ActiveButton(ICON_FA_SYNC " Reset System", false)) + { + CloseQuickMenu(); + s_host_interface->RunLater(DoReset); + } + + if (ActiveButton(ICON_FA_POWER_OFF " Exit Game", false)) + { + CloseQuickMenu(); + s_host_interface->RunLater(DoPowerOff); + } + + EndMenuButtons(); + + EndFullscreenWindow(); + } +} + +void InitializePlaceholderSaveStateListEntry(SaveStateListEntry* li, s32 slot, bool global) +{ + if (global) + li->title = StringUtil::StdStringFromFormat("Global Slot %d##global_slot_%d", slot, slot); + else + li->title = + StringUtil::StdStringFromFormat("%s Slot %d##game_slot_%d", System::GetRunningTitle().c_str(), slot, slot); + + li->summary = "No Save State"; + + std::string().swap(li->path); + std::string().swap(li->media_path); + li->slot = slot; + li->global = global; +} + +void InitializeSaveStateListEntry(SaveStateListEntry* li, CommonHostInterface::ExtendedSaveStateInfo* ssi) +{ + if (ssi->global) + { + li->title = + StringUtil::StdStringFromFormat("Global Save %d - %s##global_slot_%d", ssi->slot, ssi->title.c_str(), ssi->slot); + } + else + { + li->title = StringUtil::StdStringFromFormat("%s Slot %d##game_slot_%d", ssi->title.c_str(), ssi->slot, ssi->slot); + } + + li->summary = + StringUtil::StdStringFromFormat("%s - Saved %s", ssi->game_code.c_str(), + Timestamp::FromUnixTimestamp(ssi->timestamp).ToString("%c").GetCharArray()); + li->slot = ssi->slot; + li->global = ssi->global; + li->path = std::move(ssi->path); + li->media_path = std::move(ssi->media_path); + + li->preview_texture.reset(); + if (ssi && !ssi->screenshot_data.empty()) + { + li->preview_texture = s_host_interface->GetDisplay()->CreateTexture( + ssi->screenshot_width, ssi->screenshot_height, 1, 1, 1, HostDisplayPixelFormat::RGBA8, + ssi->screenshot_data.data(), sizeof(u32) * ssi->screenshot_width, false); + } + else + { + li->preview_texture = s_host_interface->GetDisplay()->CreateTexture( + PLACEHOLDER_ICON_WIDTH, PLACEHOLDER_ICON_HEIGHT, 1, 1, 1, HostDisplayPixelFormat::RGBA8, PLACEHOLDER_ICON_DATA, + sizeof(u32) * PLACEHOLDER_ICON_WIDTH, false); + } + + if (!li->preview_texture) + Log_ErrorPrintf("Failed to upload save state image to GPU"); +} + +void PopulateSaveStateListEntries() +{ + s_save_state_selector_slots.clear(); + + if (!System::GetRunningCode().empty()) + { + for (s32 i = 1; i <= CommonHostInterface::PER_GAME_SAVE_STATE_SLOTS; i++) + { + std::optional ssi = + s_host_interface->GetExtendedSaveStateInfo(System::GetRunningCode().c_str(), i); + + SaveStateListEntry li; + if (ssi) + InitializeSaveStateListEntry(&li, &ssi.value()); + else + InitializePlaceholderSaveStateListEntry(&li, i, false); + + s_save_state_selector_slots.push_back(std::move(li)); + } + } + + for (s32 i = 1; i <= CommonHostInterface::GLOBAL_SAVE_STATE_SLOTS; i++) + { + std::optional ssi = + s_host_interface->GetExtendedSaveStateInfo(nullptr, i); + + SaveStateListEntry li; + if (ssi) + InitializeSaveStateListEntry(&li, &ssi.value()); + else + InitializePlaceholderSaveStateListEntry(&li, i, true); + + s_save_state_selector_slots.push_back(std::move(li)); + } +} + +#if 0 +void DrawSaveStateSelector(bool is_loading, bool fullscreen) +{ + const HostDisplayTexture* selected_texture = s_placeholder_texture.get(); + if (!BeginFullscreenColumns()) + { + EndFullscreenColumns(); + return; + } + + // drawn back the front so the hover changes the image + if (BeginFullscreenColumnWindow(570.0f, LAYOUT_SCREEN_WIDTH, "save_state_selector_slots")) + { + BeginMenuButtons(static_cast(s_save_state_selector_slots.size()), true); + + for (const SaveStateListEntry& entry : s_save_state_selector_slots) + { + if (MenuButton(entry.title.c_str(), entry.summary.c_str())) + { + const std::string& path = entry.path; + s_host_interface->RunLater([path]() { s_host_interface->LoadState(path.c_str()); }); + } + + if (ImGui::IsItemHovered()) + selected_texture = entry.preview_texture.get(); + } + + EndMenuButtons(); + } + EndFullscreenColumnWindow(); + + if (BeginFullscreenColumnWindow(0.0f, 570.0f, "save_state_selector_preview", ImVec4(0.11f, 0.15f, 0.17f, 1.00f))) + { + ImGui::SetCursorPos(LayoutScale(20.0f, 20.0f)); + ImGui::PushFont(g_large_font); + ImGui::TextUnformatted(is_loading ? ICON_FA_FOLDER_OPEN " Load State" : ICON_FA_SAVE " Save State"); + ImGui::PopFont(); + + ImGui::SetCursorPos(LayoutScale(ImVec2(85.0f, 160.0f))); + ImGui::Image(selected_texture ? selected_texture->GetHandle() : s_placeholder_texture->GetHandle(), + LayoutScale(ImVec2(400.0f, 400.0f))); + + ImGui::SetCursorPosY(LayoutScale(670.0f)); + BeginMenuButtons(1, false); + if (ActiveButton(ICON_FA_BACKWARD " Back", false)) + ReturnToMainWindow(); + EndMenuButtons(); + } + EndFullscreenColumnWindow(); + + EndFullscreenColumns(); +} +#endif + +void OpenSaveStateSelector(bool is_loading) +{ + s_save_state_selector_loading = is_loading; + s_save_state_selector_open = true; + s_save_state_selector_slots.clear(); + PopulateSaveStateListEntries(); +} + +void CloseSaveStateSelector() +{ + s_save_state_selector_slots.clear(); + s_save_state_selector_open = false; +} + +void DrawSaveStateSelector(bool is_loading, bool fullscreen) +{ + if (fullscreen) + { + if (!BeginFullscreenColumns()) + { + EndFullscreenColumns(); + return; + } + + if (!BeginFullscreenColumnWindow(0.0f, LAYOUT_SCREEN_WIDTH, "save_state_selector_slots")) + { + EndFullscreenColumnWindow(); + EndFullscreenColumns(); + return; + } + } + else + { + const char* window_title = is_loading ? "Load State" : "Save State"; + + ImGui::PushFont(g_large_font); + ImGui::PushStyleVar(ImGuiStyleVar_WindowRounding, LayoutScale(10.0f)); + ImGui::PushStyleVar(ImGuiStyleVar_FramePadding, LayoutScale(ImGuiFullscreen::LAYOUT_MENU_BUTTON_X_PADDING, + ImGuiFullscreen::LAYOUT_MENU_BUTTON_Y_PADDING)); + + ImGui::SetNextWindowSize(LayoutScale(1000.0f, 680.0f)); + ImGui::SetNextWindowPos(ImGui::GetIO().DisplaySize * 0.5f, ImGuiCond_Always, ImVec2(0.5f, 0.5f)); + ImGui::OpenPopup(window_title); + bool is_open = true; + if (!ImGui::BeginPopupModal(window_title, &is_open, + ImGuiWindowFlags_NoCollapse | ImGuiWindowFlags_NoResize | ImGuiWindowFlags_NoMove) || + !is_open) + { + ImGui::PopStyleVar(2); + ImGui::PopFont(); + CloseSaveStateSelector(); + return; + } + } + + BeginMenuButtons(); + + constexpr float padding = 10.0f; + constexpr float button_height = 96.0f; + constexpr float image_width = 128.0f; + constexpr float image_height = 96.0f; + + ImDrawList* dl = ImGui::GetWindowDrawList(); + for (const SaveStateListEntry& entry : s_save_state_selector_slots) + { + ImRect bb; + bool visible, hovered; + bool pressed = MenuButtonFrame(entry.title.c_str(), true, button_height, &visible, &hovered, &bb.Min, &bb.Max); + if (!visible) + continue; + + ImVec2 pos(bb.Min); + const ImRect image_bb(pos, pos + LayoutScale(image_width, image_height)); + pos.x += LayoutScale(image_width + padding); + + dl->AddImage(static_cast(entry.preview_texture ? entry.preview_texture->GetHandle() : + s_placeholder_texture->GetHandle()), + image_bb.Min, image_bb.Max); + + ImRect text_bb(pos, ImVec2(bb.Max.x, pos.y + g_large_font->FontSize)); + ImGui::PushFont(g_large_font); + ImGui::RenderTextClipped(text_bb.Min, text_bb.Max, entry.title.c_str(), nullptr, nullptr, ImVec2(0.0f, 0.0f), + &text_bb); + ImGui::PopFont(); + + ImGui::PushFont(g_medium_font); + + if (!entry.summary.empty()) + { + text_bb.Min.y = text_bb.Max.y + LayoutScale(4.0f); + text_bb.Max.y = text_bb.Min.y + g_medium_font->FontSize; + ImGui::RenderTextClipped(text_bb.Min, text_bb.Max, entry.summary.c_str(), nullptr, nullptr, ImVec2(0.0f, 0.0f), + &text_bb); + } + + if (!entry.path.empty()) + { + text_bb.Min.y = text_bb.Max.y + LayoutScale(4.0f); + text_bb.Max.y = text_bb.Min.y + g_medium_font->FontSize; + ImGui::RenderTextClipped(text_bb.Min, text_bb.Max, entry.path.c_str(), nullptr, nullptr, ImVec2(0.0f, 0.0f), + &text_bb); + } + + if (!entry.media_path.empty()) + { + text_bb.Min.y = text_bb.Max.y + LayoutScale(4.0f); + text_bb.Max.y = text_bb.Min.y + g_medium_font->FontSize; + ImGui::RenderTextClipped(text_bb.Min, text_bb.Max, entry.media_path.c_str(), nullptr, nullptr, ImVec2(0.0f, 0.0f), + &text_bb); + } + + ImGui::PopFont(); + + if (pressed) + { + if (is_loading) + { + const std::string& path = entry.path; + s_host_interface->RunLater([path]() { + s_host_interface->LoadState(path.c_str()); + CloseSaveStateSelector(); + }); + } + else + { + const s32 slot = entry.slot; + const bool global = entry.global; + s_host_interface->RunLater([slot, global]() { + s_host_interface->SaveState(global, slot); + CloseSaveStateSelector(); + }); + } + } + } + + EndMenuButtons(); + + if (fullscreen) + { + EndFullscreenColumnWindow(); + EndFullscreenColumns(); + } + else + { + ImGui::EndPopup(); + ImGui::PopStyleVar(2); + ImGui::PopFont(); + } +} + +void DrawGameListWindow() +{ + const GameListEntry* selected_entry = nullptr; + + if (!BeginFullscreenColumns()) + { + EndFullscreenColumns(); + return; + } + + if (BeginFullscreenColumnWindow(450.0f, LAYOUT_SCREEN_WIDTH, "game_list_entries")) + { + BeginMenuButtons(); + + SmallString summary; + PathString title; + + for (const GameListEntry* entry : s_game_list_sorted_entries) + { + HostDisplayTexture* cover_texture = GetGameListCover(entry); + + title.Format("%s##%s", entry->title.c_str(), entry->path.c_str()); + + if (entry->code.empty()) + summary.Format("%s - ", Settings::GetDiscRegionName(entry->region)); + else + summary.Format("%s - %s - ", entry->code.c_str(), Settings::GetDiscRegionName(entry->region)); + + summary.AppendString(FileSystem::GetFileNameFromPath(entry->path.c_str())); + + if (MenuImageButton( + title, summary, static_cast(cover_texture->GetHandle()), + LayoutScale(ImGuiFullscreen::LAYOUT_MENU_BUTTON_HEIGHT, ImGuiFullscreen::LAYOUT_MENU_BUTTON_HEIGHT))) + { + // launch game + const std::string& path_to_launch(entry->path); + s_host_interface->RunLater([path_to_launch]() { DoStartPath(path_to_launch, true); }); + } + + if (ImGui::IsItemHovered()) + selected_entry = entry; + } + + EndMenuButtons(); + } + EndFullscreenColumnWindow(); + + if (BeginFullscreenColumnWindow(0.0f, 450.0f, "game_list_info", ImVec4(0.11f, 0.15f, 0.17f, 1.00f))) + { + const auto* window = ImGui::GetCurrentWindow(); + const ImVec2 base_pos(window->DC.CursorPos); + + ImGui::SetCursorPos(LayoutScale(ImVec2(50.0f, 50.0f))); + ImGui::Image(selected_entry ? GetGameListCover(selected_entry)->GetHandle() : + GetTextureForGameListEntryType(GameListEntryType::Count)->GetHandle(), + LayoutScale(ImVec2(350.0f, 350.0f))); + + const float work_width = ImGui::GetCurrentWindow()->WorkRect.GetWidth(); + constexpr float field_margin_y = 10.0f; + constexpr float start_x = 50.0f; + constexpr float end_x = 400.0f; + float text_y = 425.0f; + float text_width; + SmallString text; + + ImGui::SetCursorPos(LayoutScale(start_x, text_y)); + ImGui::PushStyleVar(ImGuiStyleVar_ItemSpacing, ImVec2(0.0f, field_margin_y)); + ImGui::BeginGroup(); + + if (selected_entry) + { + // title + ImGui::PushFont(g_large_font); + text_width = ImGui::CalcTextSize(selected_entry->title.c_str(), nullptr, false, work_width).x; + ImGui::SetCursorPosX((work_width - text_width) / 2.0f); + ImGui::TextWrapped("%s", selected_entry->title.c_str()); + ImGui::PopFont(); + + ImGui::PushFont(g_medium_font); + + // code + text_width = ImGui::CalcTextSize(selected_entry->code.c_str(), nullptr, false, work_width).x; + ImGui::SetCursorPosX((work_width - text_width) / 2.0f); + ImGui::TextWrapped("%s", selected_entry->code.c_str()); + ImGui::SetCursorPosY(ImGui::GetCursorPosY() + 15.0f); + + // region + ImGui::TextUnformatted("Region: "); + ImGui::SameLine(); + ImGui::Image(s_disc_region_textures[static_cast(selected_entry->region)]->GetHandle(), + LayoutScale(23.0f, 16.0f)); + ImGui::SameLine(); + ImGui::Text(" (%s)", Settings::GetDiscRegionDisplayName(selected_entry->region)); + + // compatibility + ImGui::TextUnformatted("Compatibility: "); + ImGui::SameLine(); + ImGui::Image(s_game_compatibility_textures[static_cast(selected_entry->compatibility_rating)]->GetHandle(), + LayoutScale(64.0f, 16.0f)); + ImGui::SameLine(); + ImGui::Text(" (%s)", GameList::GetGameListCompatibilityRatingString(selected_entry->compatibility_rating)); + + // size + ImGui::Text("Size: %.2f MB", static_cast(selected_entry->total_size) / 1048576.0f); + + // TODO: last played + ImGui::Text("Last Played: Never"); + + // game settings + const u32 user_setting_count = selected_entry->settings.GetUserSettingsCount(); + if (user_setting_count > 0) + ImGui::Text("%u Per-Game Settings Set", user_setting_count); + else + ImGui::TextUnformatted("No Per-Game Settings Set"); + + ImGui::PopFont(); + } + else + { + // title + const char* title = "No Game Selected"; + ImGui::PushFont(g_large_font); + text_width = ImGui::CalcTextSize(title, nullptr, false, work_width).x; + ImGui::SetCursorPosX((work_width - text_width) / 2.0f); + ImGui::TextWrapped("%s", title); + ImGui::PopFont(); + } + + ImGui::EndGroup(); + ImGui::PopStyleVar(); + + ImGui::SetCursorPosY(LayoutScale(670.0f)); + BeginMenuButtons(); + if (ActiveButton(ICON_FA_BACKWARD " Back", false)) + ReturnToMainWindow(); + EndMenuButtons(); + } + EndFullscreenColumnWindow(); + + EndFullscreenColumns(); +} + +void EnsureGameListLoaded() +{ + // not worth using a condvar here + if (s_game_list_load_thread.joinable()) + s_game_list_load_thread.join(); + + if (s_game_list_sorted_entries.empty()) + SortGameList(); +} + +static void GameListRefreshThread() +{ + ProgressCallback cb("game_list_refresh"); + s_host_interface->GetGameList()->Refresh(false, false, &cb); +} + +void QueueGameListRefresh() +{ + if (s_game_list_load_thread.joinable()) + s_game_list_load_thread.join(); + + s_game_list_sorted_entries.clear(); + s_host_interface->GetGameList()->SetSearchDirectoriesFromSettings(*s_settings_interface); + s_game_list_load_thread = std::thread(GameListRefreshThread); +} + +void SwitchToGameList() +{ + EnsureGameListLoaded(); + s_current_main_window = MainWindowType::GameList; +} + +void SortGameList() +{ + s_game_list_sorted_entries.clear(); + + for (const GameListEntry& entry : s_host_interface->GetGameList()->GetEntries()) + s_game_list_sorted_entries.push_back(&entry); + + // TODO: Custom sort types + std::sort(s_game_list_sorted_entries.begin(), s_game_list_sorted_entries.end(), + [](const GameListEntry* lhs, const GameListEntry* rhs) { return lhs->title < rhs->title; }); +} + +HostDisplayTexture* GetGameListCover(const GameListEntry* entry) +{ + // lookup and grab cover image + auto cover_it = s_cover_image_map.find(entry->path); + if (cover_it == s_cover_image_map.end()) + { + const std::string cover_path(s_host_interface->GetGameList()->GetCoverImagePathForEntry(entry)); + std::unique_ptr texture; + if (!cover_path.empty()) + { + Log_DevPrintf("Trying to load cover from '%s' for '%s'", cover_path.c_str(), entry->path.c_str()); + + Common::RGBA8Image image; + if (Common::LoadImageFromFile(&image, cover_path.c_str()) || !image.IsValid()) + { + texture = s_host_interface->GetDisplay()->CreateTexture(image.GetWidth(), image.GetHeight(), 1, 1, 1, + HostDisplayPixelFormat::RGBA8, image.GetPixels(), + image.GetByteStride()); + if (!texture) + Log_ErrorPrintf("Failed to upload %ux%u texture to GPU", image.GetWidth(), image.GetHeight()); + } + else + { + Log_ErrorPrintf("Failed to load cover from '%s'", cover_path.c_str()); + } + } + + cover_it = s_cover_image_map.emplace(entry->path, std::move(texture)).first; + } + + if (cover_it->second) + return cover_it->second.get(); + + return GetTextureForGameListEntryType(entry->type); +} + +HostDisplayTexture* GetTextureForGameListEntryType(GameListEntryType type) +{ + switch (type) + { + case GameListEntryType::PSExe: + return s_fallback_exe_texture.get(); + + case GameListEntryType::Playlist: + return s_fallback_playlist_texture.get(); + + case GameListEntryType::PSF: + return s_fallback_psf_texture.get(); + break; + + case GameListEntryType::Disc: + default: + return s_fallback_disc_texture.get(); + } +} + +HostDisplayTexture* GetCoverForCurrentGame() +{ + EnsureGameListLoaded(); + + const GameListEntry* entry = s_host_interface->GetGameList()->GetEntryForPath(System::GetRunningPath().c_str()); + if (!entry) + return s_fallback_disc_texture.get(); + + return GetGameListCover(entry); +} + +////////////////////////////////////////////////////////////////////////// +// Overlays +////////////////////////////////////////////////////////////////////////// +void DrawStatsOverlay() +{ + if (!(g_settings.display_show_fps || g_settings.display_show_vps || g_settings.display_show_speed || + g_settings.display_show_resolution || System::IsPaused() || s_host_interface->IsFastForwardEnabled() || + s_host_interface->IsTurboEnabled())) + { + return; + } + + float margin = LayoutScale(10.0f); + float position_y = margin; + ImDrawList* dl = ImGui::GetForegroundDrawList(); + TinyString text; + ImVec2 text_size; + bool first = true; + +#define DRAW_LINE(font, font_size, right_pad, color) \ + do \ + { \ + text_size = font->CalcTextSizeA(font_size, std::numeric_limits::max(), -1.0f, text, \ + text.GetCharArray() + text.GetLength(), nullptr); \ + dl->AddText(font, font_size, ImVec2(ImGui::GetIO().DisplaySize.x - (right_pad)-margin - text_size.x, position_y), \ + color, text, text.GetCharArray() + text.GetLength()); \ + position_y += text_size.y + margin; \ + } while (0) + + const System::State state = System::GetState(); + if (System::GetState() == System::State::Running) + { + const float speed = System::GetEmulationSpeed(); + if (g_settings.display_show_fps) + { + text.AppendFormattedString("%.2f", System::GetFPS()); + first = false; + } + if (g_settings.display_show_vps) + { + text.AppendFormattedString("%s%.2f", first ? "" : " / ", System::GetVPS()); + first = false; + } + if (g_settings.display_show_speed) + { + text.AppendFormattedString("%s%u%%", first ? "" : " / ", static_cast(std::round(speed))); + first = false; + } + if (!text.IsEmpty()) + { + ImU32 color; + if (speed < 95.0f) + color = IM_COL32(255, 100, 100, 255); + else if (speed > 105.0f) + color = IM_COL32(100, 255, 100, 255); + else + color = IM_COL32(255, 255, 255, 255); + + DRAW_LINE(g_large_font, g_large_font->FontSize, 0.0f, color); + } + + if (g_settings.display_show_resolution) + { + const auto [effective_width, effective_height] = g_gpu->GetEffectiveDisplayResolution(); + const bool interlaced = g_gpu->IsInterlacedDisplayEnabled(); + text.Format("%ux%u (%s)", effective_width, effective_height, interlaced ? "interlaced" : "progressive"); + DRAW_LINE(g_large_font, g_large_font->FontSize, 0.0f, IM_COL32(255, 255, 255, 255)); + } + + if (s_host_interface->IsFastForwardEnabled() || s_host_interface->IsTurboEnabled()) + { + text.Assign(ICON_FA_FAST_FORWARD); + DRAW_LINE(g_large_font, g_large_font->FontSize * 2.0f, margin, IM_COL32(255, 255, 255, 255)); + } + } + else if (state == System::State::Paused) + { + text.Assign(ICON_FA_PAUSE); + DRAW_LINE(g_large_font, g_large_font->FontSize * 2.0f, margin, IM_COL32(255, 255, 255, 255)); + } + +#undef DRAW_LINE +} + +void DrawOSDMessages() +{ + if (!g_settings.display_show_osd_messages) + { + // we still need to remove them from the queue + s_host_interface->EnumerateOSDMessages([](const std::string& message, float time_remaining) { return true; }); + return; + } + + static constexpr ImGuiWindowFlags window_flags = + ImGuiWindowFlags_NoTitleBar | ImGuiWindowFlags_NoInputs | ImGuiWindowFlags_NoMove | + ImGuiWindowFlags_NoSavedSettings | ImGuiWindowFlags_NoScrollbar | ImGuiWindowFlags_NoNav | + ImGuiWindowFlags_AlwaysAutoResize | ImGuiWindowFlags_NoFocusOnAppearing; + ImGui::PushFont(g_large_font); + ImGui::PushStyleVar(ImGuiStyleVar_WindowRounding, LayoutScale(10.0f)); + ImGui::PushStyleVar(ImGuiStyleVar_WindowPadding, LayoutScale(10.0f, 10.0f)); + + const float max_width = LayoutScale(1080.0f); + const float spacing = LayoutScale(4.0f); + const float margin = LayoutScale(10.0f); + const float padding = LayoutScale(10.0f); + float position_x = margin; + float position_y = (margin + ImGuiFullscreen::g_layout_padding_top); + u32 index = 0; + + s_host_interface->EnumerateOSDMessages( + [max_width, spacing, padding, &position_x, &position_y](const std::string& message, float time_remaining) -> bool { + const float opacity = std::min(time_remaining, 1.0f); + + if (position_y >= ImGui::GetIO().DisplaySize.y) + return false; + + const ImVec2 pos(position_x, position_y); + const ImVec2 text_size(ImGui::CalcTextSize(message.c_str(), nullptr, max_width)); + const ImVec2 size(text_size + LayoutScale(20.0f, 20.0f)); + const ImVec4 text_rect(pos.x + padding, pos.y + padding, pos.x + size.x - padding, pos.y + size.y - padding); + ImDrawList* dl = ImGui::GetForegroundDrawList(); + dl->AddRectFilled(pos, pos + size, ImGui::GetColorU32(ImGuiCol_WindowBg, opacity), LayoutScale(10.0f)); + dl->AddRect(pos, pos + size, ImGui::GetColorU32(ImGuiCol_Border), LayoutScale(10.0f)); + dl->AddText(g_large_font, g_large_font->FontSize, ImVec2(text_rect.x, text_rect.y), + ImGui::GetColorU32(ImGuiCol_Text, opacity), message.c_str(), nullptr, max_width, &text_rect); + position_y += size.y + spacing; + return true; + }); + + ImGui::PopStyleVar(2); + ImGui::PopFont(); +} + +void OpenAboutWindow() +{ + s_about_window_open = true; +} + +void DrawAboutWindow() +{ + ImGui::SetNextWindowSize(LayoutScale(1000.0f, 500.0f)); + ImGui::SetNextWindowPos(ImGui::GetIO().DisplaySize * 0.5f, ImGuiCond_Always, ImVec2(0.5f, 0.5f)); + ImGui::OpenPopup("About DuckStation"); + + ImGui::PushFont(g_large_font); + ImGui::PushStyleVar(ImGuiStyleVar_WindowRounding, LayoutScale(10.0f)); + ImGui::PushStyleVar(ImGuiStyleVar_WindowPadding, LayoutScale(10.0f, 10.0f)); + + if (ImGui::BeginPopupModal("About DuckStation", &s_about_window_open, + ImGuiWindowFlags_NoTitleBar | ImGuiWindowFlags_NoResize)) + { + ImGui::TextWrapped("DuckStation is a free and open-source simulator/emulator of the Sony PlayStation(TM) console, " + "focusing on playability, speed, and long-term maintainability."); + ImGui::NewLine(); + ImGui::TextWrapped("Contributor List: https://github.com/stenzek/duckstation/blob/master/CONTRIBUTORS.md"); + ImGui::NewLine(); + ImGui::TextWrapped("Duck icon by icons8 (https://icons8.com/icon/74847/platforms.undefined.short-title)"); + ImGui::NewLine(); + ImGui::TextWrapped("\"PlayStation\" and \"PSX\" are registered trademarks of Sony Interactive Entertainment Europe " + "Limited. This software is not affiliated in any way with Sony Interactive Entertainment."); + + ImGui::NewLine(); + + BeginMenuButtons(); + if (ActiveButton(ICON_FA_GLOBE " GitHub Repository", false)) + s_host_interface->ReportError("Go to https://github.com/stenzek/duckstation/"); + if (ActiveButton(ICON_FA_BUG " Issue Tracker", false)) + s_host_interface->ReportError("Go to https://github.com/stenzek/duckstation/issues"); + if (ActiveButton(ICON_FA_COMMENT " Discord Server", false)) + s_host_interface->ReportError("Go to https://discord.gg/Buktv3t"); + + if (ActiveButton(ICON_FA_WINDOW_CLOSE " Close", false)) + { + ImGui::CloseCurrentPopup(); + s_about_window_open = false; + } + EndMenuButtons(); + + ImGui::EndPopup(); + } + + ImGui::PopStyleVar(2); + ImGui::PopFont(); +} + +////////////////////////////////////////////////////////////////////////// +// Debug Menu +////////////////////////////////////////////////////////////////////////// + +void SetDebugMenuEnabled(bool enabled, bool save_to_ini) +{ + if (s_debug_menu_enabled == enabled) + return; + + const float size = enabled ? DPIScale(LAYOUT_MAIN_MENU_BAR_SIZE) : 0.0f; + s_host_interface->GetDisplay()->SetDisplayTopMargin(static_cast(size)); + ImGuiFullscreen::SetMenuBarSize(size); + ImGuiFullscreen::UpdateLayoutScale(); + if (ImGuiFullscreen::UpdateFonts()) + s_host_interface->GetDisplay()->UpdateImGuiFontTexture(); + s_debug_menu_enabled = enabled; + + if (save_to_ini) + { + s_settings_interface->SetBoolValue("Main", "ShowDebugMenu", enabled); + s_settings_interface->Save(); + } +} + +static void DrawDebugStats(); +static void DrawDebugSystemMenu(); +static void DrawDebugSettingsMenu(); +static void DrawDebugDebugMenu(); + +void DrawDebugMenu() +{ + if (!ImGui::BeginMainMenuBar()) + return; + + if (ImGui::BeginMenu("System")) + { + DrawDebugSystemMenu(); + ImGui::EndMenu(); + } + + if (ImGui::BeginMenu("Settings")) + { + DrawDebugSettingsMenu(); + ImGui::EndMenu(); + } + + if (ImGui::BeginMenu("Debug")) + { + DrawDebugDebugMenu(); + ImGui::EndMenu(); + } + + DrawDebugStats(); + + ImGui::EndMainMenuBar(); +} + +void DrawDebugStats() +{ + if (!System::IsShutdown()) + { + const float framebuffer_scale = ImGui::GetIO().DisplayFramebufferScale.x; + + if (System::IsPaused()) + { + ImGui::SetCursorPosX(ImGui::GetIO().DisplaySize.x - (50.0f * framebuffer_scale)); + ImGui::TextColored(ImVec4(1.0f, 1.0f, 0.0f, 1.0f), "Paused"); + } + else + { + ImGui::SetCursorPosX(ImGui::GetIO().DisplaySize.x - (420.0f * framebuffer_scale)); + ImGui::Text("Average: %.2fms", System::GetAverageFrameTime()); + + ImGui::SetCursorPosX(ImGui::GetIO().DisplaySize.x - (310.0f * framebuffer_scale)); + ImGui::Text("Worst: %.2fms", System::GetWorstFrameTime()); + + ImGui::SetCursorPosX(ImGui::GetIO().DisplaySize.x - (210.0f * framebuffer_scale)); + + const float speed = System::GetEmulationSpeed(); + const u32 rounded_speed = static_cast(std::round(speed)); + if (speed < 90.0f) + ImGui::TextColored(ImVec4(1.0f, 0.4f, 0.4f, 1.0f), "%u%%", rounded_speed); + else if (speed < 110.0f) + ImGui::TextColored(ImVec4(1.0f, 1.0f, 1.0f, 1.0f), "%u%%", rounded_speed); + else + ImGui::TextColored(ImVec4(0.4f, 1.0f, 0.4f, 1.0f), "%u%%", rounded_speed); + + ImGui::SetCursorPosX(ImGui::GetIO().DisplaySize.x - (165.0f * framebuffer_scale)); + ImGui::Text("FPS: %.2f", System::GetFPS()); + + ImGui::SetCursorPosX(ImGui::GetIO().DisplaySize.x - (80.0f * framebuffer_scale)); + ImGui::Text("VPS: %.2f", System::GetVPS()); + } + } +} + +void DrawDebugSystemMenu() +{ + const bool system_enabled = static_cast(!System::IsShutdown()); + + if (ImGui::MenuItem("Start Disc", nullptr, false, !system_enabled)) + { + DoStartFile(); + ClearImGuiFocus(); + } + + if (ImGui::MenuItem("Start BIOS", nullptr, false, !system_enabled)) + { + DoStartBIOS(); + ClearImGuiFocus(); + } + + ImGui::Separator(); + + if (ImGui::MenuItem("Power Off", nullptr, false, system_enabled)) + { + DoPowerOff(); + ClearImGuiFocus(); + } + + if (ImGui::MenuItem("Reset", nullptr, false, system_enabled)) + { + DoReset(); + ClearImGuiFocus(); + } + + if (ImGui::MenuItem("Pause", nullptr, System::IsPaused(), system_enabled)) + { + DoPause(); + ClearImGuiFocus(); + } + + ImGui::Separator(); + + if (ImGui::MenuItem("Change Disc", nullptr, false, system_enabled)) + { +#if 0 + DoChangeDisc(); +#endif + ClearImGuiFocus(); + } + + if (ImGui::MenuItem("Remove Disc", nullptr, false, system_enabled)) + { + s_host_interface->RunLater([]() { System::RemoveMedia(); }); + ClearImGuiFocus(); + } + + if (ImGui::MenuItem("Frame Step", nullptr, false, system_enabled)) + { +#if 0 + s_host_interface->RunLater([]() { DoFrameStep(); }); +#endif + ClearImGuiFocus(); + } + + ImGui::Separator(); + + if (ImGui::BeginMenu("Load State")) + { + for (u32 i = 1; i <= CommonHostInterface::GLOBAL_SAVE_STATE_SLOTS; i++) + { + char buf[16]; + std::snprintf(buf, sizeof(buf), "State %u", i); + if (ImGui::MenuItem(buf)) + { + s_host_interface->RunLater([i]() { s_host_interface->LoadState(true, i); }); + ClearImGuiFocus(); + } + } + ImGui::EndMenu(); + } + + if (ImGui::BeginMenu("Save State", system_enabled)) + { + for (u32 i = 1; i <= CommonHostInterface::GLOBAL_SAVE_STATE_SLOTS; i++) + { + TinyString buf; + buf.Format("State %u", i); + if (ImGui::MenuItem(buf)) + { + s_host_interface->RunLater([i]() { s_host_interface->SaveState(true, i); }); + ClearImGuiFocus(); + } + } + ImGui::EndMenu(); + } + + ImGui::Separator(); + + if (ImGui::BeginMenu("Cheats", system_enabled)) + { + const bool has_cheat_file = System::HasCheatList(); + if (ImGui::BeginMenu("Enabled Cheats", has_cheat_file)) + { + CheatList* cl = System::GetCheatList(); + for (u32 i = 0; i < cl->GetCodeCount(); i++) + { + const CheatCode& cc = cl->GetCode(i); + if (ImGui::MenuItem(cc.description.c_str(), nullptr, cc.enabled, true)) + s_host_interface->SetCheatCodeState(i, !cc.enabled, g_settings.auto_load_cheats); + } + + ImGui::EndMenu(); + } + + if (ImGui::BeginMenu("Apply Cheat", has_cheat_file)) + { + CheatList* cl = System::GetCheatList(); + for (u32 i = 0; i < cl->GetCodeCount(); i++) + { + const CheatCode& cc = cl->GetCode(i); + if (ImGui::MenuItem(cc.description.c_str())) + s_host_interface->ApplyCheatCode(i); + } + + ImGui::EndMenu(); + } + + ImGui::EndMenu(); + } + + ImGui::Separator(); + + if (ImGui::MenuItem("Exit")) + s_host_interface->RequestExit(); +} + +void DrawDebugSettingsMenu() +{ + bool settings_changed = false; + + if (ImGui::BeginMenu("CPU Execution Mode")) + { + const CPUExecutionMode current = s_settings_copy.cpu_execution_mode; + for (u32 i = 0; i < static_cast(CPUExecutionMode::Count); i++) + { + if (ImGui::MenuItem(Settings::GetCPUExecutionModeDisplayName(static_cast(i)), nullptr, + i == static_cast(current))) + { + s_settings_copy.cpu_execution_mode = static_cast(i); + settings_changed = true; + } + } + + ImGui::EndMenu(); + } + + if (ImGui::MenuItem("CPU Clock Control", nullptr, &s_settings_copy.cpu_overclock_enable)) + { + settings_changed = true; + s_settings_copy.UpdateOverclockActive(); + } + + if (ImGui::BeginMenu("CPU Clock Speed")) + { + static constexpr auto values = make_array(10u, 25u, 50u, 75u, 100u, 125u, 150u, 175u, 200u, 225u, 250u, 275u, 300u, + 350u, 400u, 450u, 500u, 600u, 700u, 800u); + const u32 percent = s_settings_copy.GetCPUOverclockPercent(); + for (u32 value : values) + { + if (ImGui::MenuItem(TinyString::FromFormat("%u%%", value), nullptr, percent == value)) + { + s_settings_copy.SetCPUOverclockPercent(value); + s_settings_copy.UpdateOverclockActive(); + settings_changed = true; + } + } + + ImGui::EndMenu(); + } + + settings_changed |= + ImGui::MenuItem("Recompiler Memory Exceptions", nullptr, &s_settings_copy.cpu_recompiler_memory_exceptions); + if (ImGui::BeginMenu("Recompiler Fastmem")) + { + for (u32 i = 0; i < static_cast(CPUFastmemMode::Count); i++) + { + if (ImGui::MenuItem(Settings::GetCPUFastmemModeDisplayName(static_cast(i)), nullptr, + s_settings_copy.cpu_fastmem_mode == static_cast(i))) + { + s_settings_copy.cpu_fastmem_mode = static_cast(i); + settings_changed = true; + } + } + + ImGui::EndMenu(); + } + + settings_changed |= ImGui::MenuItem("Recompiler ICache", nullptr, &s_settings_copy.cpu_recompiler_icache); + + ImGui::Separator(); + + if (ImGui::BeginMenu("Renderer")) + { + const GPURenderer current = s_settings_copy.gpu_renderer; + for (u32 i = 0; i < static_cast(GPURenderer::Count); i++) + { + if (ImGui::MenuItem(Settings::GetRendererDisplayName(static_cast(i)), nullptr, + i == static_cast(current))) + { + s_settings_copy.gpu_renderer = static_cast(i); + settings_changed = true; + } + } + + settings_changed |= ImGui::MenuItem("GPU on Thread", nullptr, &s_settings_copy.gpu_use_thread); + + ImGui::EndMenu(); + } + + bool fullscreen = s_host_interface->IsFullscreen(); + if (ImGui::MenuItem("Fullscreen", nullptr, &fullscreen)) + s_host_interface->RunLater([fullscreen] { s_host_interface->SetFullscreen(fullscreen); }); + + if (ImGui::BeginMenu("Resize to Game", System::IsValid())) + { + static constexpr auto scales = make_array(1, 2, 3, 4, 5, 6, 7, 8, 9, 10); + for (const u32 scale : scales) + { + if (ImGui::MenuItem(TinyString::FromFormat("%ux Scale", scale))) + s_host_interface->RunLater( + [scale]() { s_host_interface->RequestRenderWindowScale(static_cast(scale)); }); + } + + ImGui::EndMenu(); + } + + settings_changed |= ImGui::MenuItem("VSync", nullptr, &s_settings_copy.video_sync_enabled); + + ImGui::Separator(); + + if (ImGui::BeginMenu("Resolution Scale")) + { + const u32 current_internal_resolution = s_settings_copy.gpu_resolution_scale; + for (u32 scale = 1; scale <= GPU::MAX_RESOLUTION_SCALE; scale++) + { + char buf[32]; + std::snprintf(buf, sizeof(buf), "%ux (%ux%u)", scale, scale * VRAM_WIDTH, scale * VRAM_HEIGHT); + + if (ImGui::MenuItem(buf, nullptr, current_internal_resolution == scale)) + { + s_settings_copy.gpu_resolution_scale = scale; + settings_changed = true; + } + } + + ImGui::EndMenu(); + } + + if (ImGui::BeginMenu("Multisampling")) + { + const u32 current_multisamples = s_settings_copy.gpu_multisamples; + const bool current_ssaa = s_settings_copy.gpu_per_sample_shading; + + if (ImGui::MenuItem("None", nullptr, (current_multisamples == 1))) + { + s_settings_copy.gpu_multisamples = 1; + s_settings_copy.gpu_per_sample_shading = false; + settings_changed = true; + } + + for (u32 i = 2; i <= 32; i *= 2) + { + char buf[32]; + std::snprintf(buf, sizeof(buf), "%ux MSAA", i); + + if (ImGui::MenuItem(buf, nullptr, (current_multisamples == i && !current_ssaa))) + { + s_settings_copy.gpu_multisamples = i; + s_settings_copy.gpu_per_sample_shading = false; + settings_changed = true; + } + } + + for (u32 i = 2; i <= 32; i *= 2) + { + char buf[32]; + std::snprintf(buf, sizeof(buf), "%ux SSAA", i); + + if (ImGui::MenuItem(buf, nullptr, (current_multisamples == i && current_ssaa))) + { + s_settings_copy.gpu_multisamples = i; + s_settings_copy.gpu_per_sample_shading = true; + settings_changed = true; + } + } + + ImGui::EndMenu(); + } + + if (ImGui::BeginMenu("PGXP")) + { + settings_changed |= ImGui::MenuItem("PGXP Enabled", nullptr, &s_settings_copy.gpu_pgxp_enable); + settings_changed |= + ImGui::MenuItem("PGXP Culling", nullptr, &s_settings_copy.gpu_pgxp_culling, s_settings_copy.gpu_pgxp_enable); + settings_changed |= ImGui::MenuItem("PGXP Texture Correction", nullptr, + &s_settings_copy.gpu_pgxp_texture_correction, s_settings_copy.gpu_pgxp_enable); + settings_changed |= ImGui::MenuItem("PGXP Vertex Cache", nullptr, &s_settings_copy.gpu_pgxp_vertex_cache, + s_settings_copy.gpu_pgxp_enable); + settings_changed |= + ImGui::MenuItem("PGXP CPU Instructions", nullptr, &s_settings_copy.gpu_pgxp_cpu, s_settings_copy.gpu_pgxp_enable); + settings_changed |= ImGui::MenuItem("PGXP Preserve Projection Precision", nullptr, + &s_settings_copy.gpu_pgxp_preserve_proj_fp, s_settings_copy.gpu_pgxp_enable); + settings_changed |= ImGui::MenuItem("PGXP Depth Buffer", nullptr, &s_settings_copy.gpu_pgxp_depth_buffer, + s_settings_copy.gpu_pgxp_enable); + ImGui::EndMenu(); + } + + settings_changed |= ImGui::MenuItem("True (24-Bit) Color", nullptr, &s_settings_copy.gpu_true_color); + settings_changed |= ImGui::MenuItem("Scaled Dithering", nullptr, &s_settings_copy.gpu_scaled_dithering); + + if (ImGui::BeginMenu("Texture Filtering")) + { + const GPUTextureFilter current = s_settings_copy.gpu_texture_filter; + for (u32 i = 0; i < static_cast(GPUTextureFilter::Count); i++) + { + if (ImGui::MenuItem(Settings::GetTextureFilterDisplayName(static_cast(i)), nullptr, + i == static_cast(current))) + { + s_settings_copy.gpu_texture_filter = static_cast(i); + settings_changed = true; + } + } + + ImGui::EndMenu(); + } + + ImGui::Separator(); + + settings_changed |= ImGui::MenuItem("Disable Interlacing", nullptr, &s_settings_copy.gpu_disable_interlacing); + settings_changed |= ImGui::MenuItem("Widescreen Hack", nullptr, &s_settings_copy.gpu_widescreen_hack); + settings_changed |= ImGui::MenuItem("Force NTSC Timings", nullptr, &s_settings_copy.gpu_force_ntsc_timings); + settings_changed |= ImGui::MenuItem("24-Bit Chroma Smoothing", nullptr, &s_settings_copy.gpu_24bit_chroma_smoothing); + + ImGui::Separator(); + + settings_changed |= ImGui::MenuItem("Display Linear Filtering", nullptr, &s_settings_copy.display_linear_filtering); + settings_changed |= ImGui::MenuItem("Display Integer Scaling", nullptr, &s_settings_copy.display_integer_scaling); + + if (ImGui::BeginMenu("Aspect Ratio")) + { + for (u32 i = 0; i < static_cast(DisplayAspectRatio::Count); i++) + { + if (ImGui::MenuItem(Settings::GetDisplayAspectRatioName(static_cast(i)), nullptr, + s_settings_copy.display_aspect_ratio == static_cast(i))) + { + s_settings_copy.display_aspect_ratio = static_cast(i); + settings_changed = true; + } + } + + ImGui::EndMenu(); + } + + if (ImGui::BeginMenu("Crop Mode")) + { + for (u32 i = 0; i < static_cast(DisplayCropMode::Count); i++) + { + if (ImGui::MenuItem(Settings::GetDisplayCropModeDisplayName(static_cast(i)), nullptr, + s_settings_copy.display_crop_mode == static_cast(i))) + { + s_settings_copy.display_crop_mode = static_cast(i); + settings_changed = true; + } + } + + ImGui::EndMenu(); + } + + if (ImGui::BeginMenu("Downsample Mode")) + { + for (u32 i = 0; i < static_cast(GPUDownsampleMode::Count); i++) + { + if (ImGui::MenuItem(Settings::GetDownsampleModeDisplayName(static_cast(i)), nullptr, + s_settings_copy.gpu_downsample_mode == static_cast(i))) + { + s_settings_copy.gpu_downsample_mode = static_cast(i); + settings_changed = true; + } + } + + ImGui::EndMenu(); + } + + settings_changed |= ImGui::MenuItem("Force 4:3 For 24-bit", nullptr, &s_settings_copy.display_force_4_3_for_24bit); + + ImGui::Separator(); + + if (ImGui::MenuItem("Dump Audio", nullptr, s_host_interface->IsDumpingAudio(), System::IsValid())) + { + if (!s_host_interface->IsDumpingAudio()) + s_host_interface->StartDumpingAudio(); + else + s_host_interface->StopDumpingAudio(); + } + + if (ImGui::MenuItem("Save Screenshot")) + s_host_interface->RunLater([]() { s_host_interface->SaveScreenshot(); }); + + if (settings_changed) + s_host_interface->RunLater(SaveAndApplySettings); +} + +void DrawDebugDebugMenu() +{ + const bool system_valid = System::IsValid(); + Settings::DebugSettings& debug_settings = g_settings.debugging; + bool settings_changed = false; + + if (ImGui::BeginMenu("Log Level")) + { + for (u32 i = LOGLEVEL_NONE; i < LOGLEVEL_COUNT; i++) + { + if (ImGui::MenuItem(Settings::GetLogLevelDisplayName(static_cast(i)), nullptr, + g_settings.log_level == static_cast(i))) + { + s_settings_copy.log_level = static_cast(i); + settings_changed = true; + } + } + + ImGui::EndMenu(); + } + + settings_changed |= ImGui::MenuItem("Log To Console", nullptr, &s_settings_copy.log_to_console); + settings_changed |= ImGui::MenuItem("Log To Debug", nullptr, &s_settings_copy.log_to_debug); + settings_changed |= ImGui::MenuItem("Log To File", nullptr, &s_settings_copy.log_to_file); + + ImGui::Separator(); + + settings_changed |= ImGui::MenuItem("Disable All Enhancements", nullptr, &s_settings_copy.disable_all_enhancements); + settings_changed |= ImGui::MenuItem("Dump CPU to VRAM Copies", nullptr, &debug_settings.dump_cpu_to_vram_copies); + settings_changed |= ImGui::MenuItem("Dump VRAM to CPU Copies", nullptr, &debug_settings.dump_vram_to_cpu_copies); + + if (ImGui::MenuItem("CPU Trace Logging", nullptr, CPU::IsTraceEnabled())) + { + if (!CPU::IsTraceEnabled()) + CPU::StartTrace(); + else + CPU::StopTrace(); + } + + ImGui::Separator(); + + settings_changed |= ImGui::MenuItem("Show VRAM", nullptr, &debug_settings.show_vram); + settings_changed |= ImGui::MenuItem("Show GPU State", nullptr, &debug_settings.show_gpu_state); + settings_changed |= ImGui::MenuItem("Show CDROM State", nullptr, &debug_settings.show_cdrom_state); + settings_changed |= ImGui::MenuItem("Show SPU State", nullptr, &debug_settings.show_spu_state); + settings_changed |= ImGui::MenuItem("Show Timers State", nullptr, &debug_settings.show_timers_state); + settings_changed |= ImGui::MenuItem("Show MDEC State", nullptr, &debug_settings.show_mdec_state); + settings_changed |= ImGui::MenuItem("Show DMA State", nullptr, &debug_settings.show_dma_state); + + if (settings_changed) + { + // have to apply it to the copy too, otherwise it won't save + Settings::DebugSettings& debug_settings_copy = s_settings_copy.debugging; + debug_settings_copy.show_gpu_state = debug_settings.show_gpu_state; + debug_settings_copy.show_vram = debug_settings.show_vram; + debug_settings_copy.dump_cpu_to_vram_copies = debug_settings.dump_cpu_to_vram_copies; + debug_settings_copy.dump_vram_to_cpu_copies = debug_settings.dump_vram_to_cpu_copies; + debug_settings_copy.show_cdrom_state = debug_settings.show_cdrom_state; + debug_settings_copy.show_spu_state = debug_settings.show_spu_state; + debug_settings_copy.show_timers_state = debug_settings.show_timers_state; + debug_settings_copy.show_mdec_state = debug_settings.show_mdec_state; + debug_settings_copy.show_dma_state = debug_settings.show_dma_state; + s_host_interface->RunLater(SaveAndApplySettings); + } +} + +bool SetControllerNavInput(FrontendCommon::ControllerNavigationButton button, bool value) +{ + s_nav_input_values[static_cast(button)] = value; + if (!HasActiveWindow()) + return false; + + // This is a bit hacky.. + ImGuiIO& io = ImGui::GetIO(); + +#define MAP_KEY(nbutton, imkey) \ + if (button == nbutton) \ + { \ + io.KeysDown[io.KeyMap[imkey]] = value; \ + } + + MAP_KEY(FrontendCommon::ControllerNavigationButton::LeftShoulder, ImGuiKey_PageUp); + MAP_KEY(FrontendCommon::ControllerNavigationButton::RightShoulder, ImGuiKey_PageDown); + +#undef MAP_KEY + + return true; +} + +void SetImGuiNavInputs() +{ + if (!HasActiveWindow()) + return; + + ImGuiIO& io = ImGui::GetIO(); + +#define MAP_BUTTON(button, imbutton) io.NavInputs[imbutton] = s_nav_input_values[static_cast(button)] ? 1.0f : 0.0f + + MAP_BUTTON(FrontendCommon::ControllerNavigationButton::Activate, ImGuiNavInput_Activate); + MAP_BUTTON(FrontendCommon::ControllerNavigationButton::Cancel, ImGuiNavInput_Cancel); + MAP_BUTTON(FrontendCommon::ControllerNavigationButton::DPadLeft, ImGuiNavInput_DpadLeft); + MAP_BUTTON(FrontendCommon::ControllerNavigationButton::DPadRight, ImGuiNavInput_DpadRight); + MAP_BUTTON(FrontendCommon::ControllerNavigationButton::DPadUp, ImGuiNavInput_DpadUp); + MAP_BUTTON(FrontendCommon::ControllerNavigationButton::DPadDown, ImGuiNavInput_DpadDown); + +#undef MAP_BUTTON +} + +} // namespace FullscreenUI diff --git a/src/frontend-common/fullscreen_ui.h b/src/frontend-common/fullscreen_ui.h new file mode 100644 index 000000000..738a05773 --- /dev/null +++ b/src/frontend-common/fullscreen_ui.h @@ -0,0 +1,63 @@ +#pragma once +#include "common/types.h" + +class CommonHostInterface; +class SettingsInterface; +struct Settings; + +namespace FrontendCommon { +enum class ControllerNavigationButton : u32; +} + +namespace FullscreenUI { +enum class MainWindowType +{ + None, + Landing, + GameList, + Settings, + QuickMenu, + MoreQuickMenu +}; + +enum class SettingsPage +{ + InterfaceSettings, + GameListSettings, + ConsoleSettings, + EmulationSettings, + BIOSSettings, + ControllerSettings, + HotkeySettings, + MemoryCardSettings, + DisplaySettings, + EnhancementSettings, + AudioSettings, + AdvancedSettings, + Count +}; + +bool Initialize(CommonHostInterface* host_interface, SettingsInterface* settings_interface); +bool HasActiveWindow(); +void SystemCreated(); +void SystemDestroyed(); +void SystemPaused(bool paused); +void OpenQuickMenu(); +void CloseQuickMenu(); +void Shutdown(); +void Render(); + +void EnsureGameListLoaded(); + +Settings& GetSettingsCopy(); +void SaveAndApplySettings(); +void SetDebugMenuEnabled(bool enabled, bool save_to_ini = false); + +/// Only ImGuiNavInput_Activate, ImGuiNavInput_Cancel, and DPad should be forwarded. +/// Returns true if the UI consumed the event, and it should not execute the normal handler. +bool SetControllerNavInput(FrontendCommon::ControllerNavigationButton button, bool value); + +/// Forwards the controller navigation to ImGui for fullscreen navigation. Call before NewFrame(). +void SetImGuiNavInputs(); + +} // namespace FullscreenUI diff --git a/src/frontend-common/fullscreen_ui_progress_callback.cpp b/src/frontend-common/fullscreen_ui_progress_callback.cpp new file mode 100644 index 000000000..a658dda83 --- /dev/null +++ b/src/frontend-common/fullscreen_ui_progress_callback.cpp @@ -0,0 +1,117 @@ +#include "fullscreen_ui_progress_callback.h" +#include "common/log.h" +#include "core/host_interface.h" +#include "imgui_fullscreen.h" +Log_SetChannel(ProgressCallback); + +namespace FullscreenUI { + +ProgressCallback::ProgressCallback(String name) : BaseProgressCallback(), m_name(std::move(name)) +{ + ImGuiFullscreen::OpenBackgroundProgressDialog(m_name, "", 0, 100, 0); +} + +ProgressCallback::~ProgressCallback() +{ + ImGuiFullscreen::CloseBackgroundProgressDialog(m_name); +} + +void ProgressCallback::PushState() +{ + BaseProgressCallback::PushState(); +} + +void ProgressCallback::PopState() +{ + BaseProgressCallback::PopState(); + Redraw(true); +} + +void ProgressCallback::SetCancellable(bool cancellable) +{ + BaseProgressCallback::SetCancellable(cancellable); + Redraw(true); +} + +void ProgressCallback::SetTitle(const char* title) +{ + // todo? +} + +void ProgressCallback::SetStatusText(const char* text) +{ + BaseProgressCallback::SetStatusText(text); + Redraw(true); +} + +void ProgressCallback::SetProgressRange(u32 range) +{ + u32 last_range = m_progress_range; + + BaseProgressCallback::SetProgressRange(range); + + if (m_progress_range != last_range) + Redraw(false); +} + +void ProgressCallback::SetProgressValue(u32 value) +{ + u32 lastValue = m_progress_value; + + BaseProgressCallback::SetProgressValue(value); + + if (m_progress_value != lastValue) + Redraw(false); +} + +void ProgressCallback::Redraw(bool force) +{ + const int percent = + static_cast((static_cast(m_progress_value) / static_cast(m_progress_range)) * 100.0f); + if (percent == m_last_progress_percent && !force) + return; + + m_last_progress_percent = percent; + ImGuiFullscreen::UpdateBackgroundProgressDialog( + m_name, m_status_text.GetCharArray(), 0, 100, percent); +} + +void ProgressCallback::DisplayError(const char* message) +{ + Log_ErrorPrint(message); +} + +void ProgressCallback::DisplayWarning(const char* message) +{ + Log_WarningPrint(message); +} + +void ProgressCallback::DisplayInformation(const char* message) +{ + Log_InfoPrint(message); +} + +void ProgressCallback::DisplayDebugMessage(const char* message) +{ + Log_DevPrint(message); +} + +void ProgressCallback::ModalError(const char* message) +{ + Log_ErrorPrint(message); + g_host_interface->ReportError(message); +} + +bool ProgressCallback::ModalConfirmation(const char* message) +{ + Log_InfoPrint(message); + return g_host_interface->ConfirmMessage(message); +} + +void ProgressCallback::ModalInformation(const char* message) +{ + Log_InfoPrint(message); + g_host_interface->ReportMessage(message); +} + +} // namespace FullscreenUI \ No newline at end of file diff --git a/src/frontend-common/fullscreen_ui_progress_callback.h b/src/frontend-common/fullscreen_ui_progress_callback.h new file mode 100644 index 000000000..6647e495a --- /dev/null +++ b/src/frontend-common/fullscreen_ui_progress_callback.h @@ -0,0 +1,38 @@ +#pragma once +#include "common/progress_callback.h" +#include "common/string.h" + +namespace FullscreenUI { + +class ProgressCallback final : public BaseProgressCallback +{ +public: + ProgressCallback(String name); + ~ProgressCallback() override; + + void PushState() override; + void PopState() override; + + void SetCancellable(bool cancellable) override; + void SetTitle(const char* title) override; + void SetStatusText(const char* text) override; + void SetProgressRange(u32 range) override; + void SetProgressValue(u32 value) override; + + void DisplayError(const char* message) override; + void DisplayWarning(const char* message) override; + void DisplayInformation(const char* message) override; + void DisplayDebugMessage(const char* message) override; + + void ModalError(const char* message) override; + bool ModalConfirmation(const char* message) override; + void ModalInformation(const char* message) override; + +private: + void Redraw(bool force); + + String m_name; + int m_last_progress_percent = -1; +}; + +} // namespace FullscreenUI \ No newline at end of file diff --git a/src/frontend-common/imgui_fullscreen.cpp b/src/frontend-common/imgui_fullscreen.cpp new file mode 100644 index 000000000..6389e1487 --- /dev/null +++ b/src/frontend-common/imgui_fullscreen.cpp @@ -0,0 +1,1262 @@ +#define IMGUI_DEFINE_MATH_OPERATORS + +#include "imgui_fullscreen.h" +#include "IconsFontAwesome5.h" +#include "common/assert.h" +#include "common/file_system.h" +#include "common/string.h" +#include "common/string_util.h" +#include "core/host_display.h" +#include "core/host_interface.h" +#include "imgui_internal.h" +#include "imgui_styles.h" +#include +#include + +namespace ImGuiFullscreen { +static void DrawFileSelector(); +static void DrawChoiceDialog(); +static void DrawBackgroundProgressDialogs(); + +ImFont* g_standard_font = nullptr; +ImFont* g_medium_font = nullptr; +ImFont* g_large_font = nullptr; +ImFont* g_icon_font = nullptr; + +float g_layout_scale = 1.0f; +float g_layout_padding_left = 0.0f; +float g_layout_padding_top = 0.0f; + +static float s_menu_bar_size; +static std::string s_font_filename; +static std::string s_icon_font_filename; +static std::vector s_icon_font_data; +static float s_font_size = 15.0f; +static const ImWchar* s_font_glyph_range = nullptr; + +static u32 s_menu_button_index = 0; + +static ImRect PadRect(const ImRect& r, float padding) +{ + return ImRect(ImVec2(r.Min.x + padding, r.Min.y + padding), ImVec2(r.Max.x - padding, r.Max.y - padding)); +} + +void SetFontFilename(const char* filename) +{ + if (filename) + s_font_filename = filename; + else + std::string().swap(s_font_filename); +} + +void SetIconFontFilename(const char* icon_font_filename) +{ + if (icon_font_filename) + s_icon_font_filename = icon_font_filename; + else + std::string().swap(s_icon_font_filename); +} + +void SetIconFontData(std::vector data) +{ + s_icon_font_data = std::move(data); +} + +void SetFontSize(float size_pixels) +{ + s_font_size = size_pixels; +} + +void SetFontGlyphRanges(const ImWchar* glyph_ranges) +{ + s_font_glyph_range = glyph_ranges; +} + +void SetMenuBarSize(float size) +{ + if (s_menu_bar_size == size) + return; + + s_menu_bar_size = size; +} + +static void AddIconFonts(float size) +{ + static const ImWchar range_fa[] = {ICON_MIN_FA, ICON_MAX_FA, 0}; + + ImFontConfig cfg; + cfg.MergeMode = true; + cfg.PixelSnapH = true; + cfg.GlyphMinAdvanceX = size * 0.75f; + cfg.GlyphMaxAdvanceX = size * 0.75f; + + if (!s_icon_font_data.empty()) + { + cfg.FontDataOwnedByAtlas = false; + ImGui::GetIO().Fonts->AddFontFromMemoryTTF(s_icon_font_data.data(), static_cast(s_icon_font_data.size()), + size * 0.75f, &cfg, range_fa); + } + else + { + ImGui::GetIO().Fonts->AddFontFromFileTTF(s_icon_font_filename.c_str(), size * 0.75f, &cfg, range_fa); + } +} + +bool UpdateFonts() +{ + const float standard_font_size = std::round(DPIScale(s_font_size)); + const float medium_font_size = std::round(LayoutScale(LAYOUT_MEDIUM_FONT_SIZE)); + const float large_font_size = std::round(LayoutScale(LAYOUT_LARGE_FONT_SIZE)); + + if (g_standard_font && g_standard_font->FontSize == standard_font_size && medium_font_size && + g_medium_font->FontSize == medium_font_size && large_font_size && g_large_font->FontSize == large_font_size) + { + return false; + } + + ImGuiIO& io = ImGui::GetIO(); + io.Fonts->Clear(); + + if (s_font_filename.empty()) + { + g_standard_font = ImGui::AddRobotoRegularFont(standard_font_size); + AddIconFonts(standard_font_size); + g_medium_font = ImGui::AddRobotoRegularFont(medium_font_size); + AddIconFonts(medium_font_size); + g_large_font = ImGui::AddRobotoRegularFont(large_font_size); + AddIconFonts(large_font_size); + } + else + { + g_standard_font = + io.Fonts->AddFontFromFileTTF(s_font_filename.c_str(), standard_font_size, nullptr, s_font_glyph_range); + AddIconFonts(standard_font_size); + g_medium_font = + io.Fonts->AddFontFromFileTTF(s_font_filename.c_str(), medium_font_size, nullptr, s_font_glyph_range); + AddIconFonts(medium_font_size); + g_large_font = io.Fonts->AddFontFromFileTTF(s_font_filename.c_str(), large_font_size, nullptr, s_font_glyph_range); + AddIconFonts(large_font_size); + } + + if (!io.Fonts->Build()) + Panic("Failed to rebuild font atlas"); + + return true; +} + +bool UpdateLayoutScale() +{ + static constexpr float LAYOUT_RATIO = LAYOUT_SCREEN_WIDTH / LAYOUT_SCREEN_HEIGHT; + const ImGuiIO& io = ImGui::GetIO(); + + const float screen_width = io.DisplaySize.x; + const float screen_height = io.DisplaySize.y - s_menu_bar_size; + const float screen_ratio = screen_width / screen_height; + const float old_scale = g_layout_scale; + + if (screen_ratio > LAYOUT_RATIO) + { + // screen is wider, use height, pad width + g_layout_scale = screen_height / LAYOUT_SCREEN_HEIGHT; + g_layout_padding_top = s_menu_bar_size; + g_layout_padding_left = (screen_width - (LAYOUT_SCREEN_WIDTH * g_layout_scale)) / 2.0f; + } + else + { + // screen is taller, use width, pad height + g_layout_scale = screen_width / LAYOUT_SCREEN_WIDTH; + g_layout_padding_top = (screen_height - (LAYOUT_SCREEN_HEIGHT * g_layout_scale)) / 2.0f + s_menu_bar_size; + g_layout_padding_left = 0.0f; + } + + return g_layout_scale != old_scale; +} + +void BeginLayout() +{ + ImGui::PushStyleVar(ImGuiStyleVar_WindowBorderSize, 0.0f); + ImGui::PushStyleVar(ImGuiStyleVar_WindowRounding, 0.0f); + ImGui::PushStyleColor(ImGuiCol_Text, UIPrimaryTextColor()); + ImGui::PushStyleColor(ImGuiCol_Button, UIPrimaryLineColor()); + ImGui::PushStyleColor(ImGuiCol_ButtonActive, UISecondaryDarkColor()); + ImGui::PushStyleColor(ImGuiCol_ButtonHovered, UISecondaryColor()); + ImGui::PushStyleColor(ImGuiCol_Border, UISecondaryLightColor()); +} + +void EndLayout() +{ + DrawFileSelector(); + DrawChoiceDialog(); + DrawBackgroundProgressDialogs(); + + ImGui::PopStyleColor(5); + ImGui::PopStyleVar(2); +} + +bool BeginFullscreenColumns(const char* title) +{ + ImGui::SetNextWindowPos(ImVec2(g_layout_padding_left, g_layout_padding_top)); + ImGui::SetNextWindowSize(LayoutScale(ImVec2(LAYOUT_SCREEN_WIDTH, LAYOUT_SCREEN_HEIGHT))); + + ImGui::PushStyleVar(ImGuiStyleVar_WindowPadding, ImVec2(0.0f, 0.0f)); + ImGui::PushStyleVar(ImGuiStyleVar_WindowBorderSize, 0.0f); + ImGui::PushStyleVar(ImGuiStyleVar_WindowRounding, 0.0f); + + bool clipped; + if (title) + { + ImGui::PushFont(g_large_font); + clipped = ImGui::Begin(title, nullptr, ImGuiWindowFlags_NoCollapse | ImGuiWindowFlags_NoResize); + ImGui::PopFont(); + } + else + { + clipped = ImGui::Begin("fullscreen_ui_columns_parent", nullptr, + ImGuiWindowFlags_NoTitleBar | ImGuiWindowFlags_NoCollapse | ImGuiWindowFlags_NoResize); + } + + return clipped; +} + +void EndFullscreenColumns() +{ + ImGui::End(); + ImGui::PopStyleVar(3); +} + +bool BeginFullscreenColumnWindow(float start, float end, const char* name, const ImVec4& background) +{ + const ImVec2 pos(LayoutScale(start), 0.0f); + const ImVec2 size(LayoutScale(ImVec2(end - start, LAYOUT_SCREEN_HEIGHT))); + + ImGui::PushStyleColor(ImGuiCol_ChildBg, background); + + ImGui::SetCursorPos(pos); + + return ImGui::BeginChild(name, size, false, ImGuiWindowFlags_NavFlattened); +} + +void EndFullscreenColumnWindow() +{ + ImGui::EndChild(); + ImGui::PopStyleColor(); +} + +bool BeginFullscreenWindow(float left, float top, float width, float height, const char* name, + const ImVec4& background /* = HEX_TO_IMVEC4(0x212121, 0xFF) */, float rounding /*= 0.0f*/, + float padding /*= 0.0f*/, ImGuiWindowFlags flags /*= 0*/) +{ + if (left < 0.0f) + left = (LAYOUT_SCREEN_WIDTH - width) * -left; + if (top < 0.0f) + top = (LAYOUT_SCREEN_HEIGHT - height) * -top; + + ImGui::SetNextWindowSize(LayoutScale(ImVec2(width, height))); + ImGui::SetNextWindowPos(ImVec2(LayoutScale(left) + g_layout_padding_left, LayoutScale(top) + g_layout_padding_top)); + + ImGui::PushStyleColor(ImGuiCol_WindowBg, background); + ImGui::PushStyleVar(ImGuiStyleVar_WindowPadding, LayoutScale(padding, padding)); + ImGui::PushStyleVar(ImGuiStyleVar_WindowBorderSize, 0.0f); + ImGui::PushStyleVar(ImGuiStyleVar_WindowRounding, LayoutScale(rounding)); + + return ImGui::Begin(name, nullptr, + ImGuiWindowFlags_NoTitleBar | ImGuiWindowFlags_NoCollapse | ImGuiWindowFlags_NoResize | + ImGuiWindowFlags_NoBringToFrontOnFocus | flags); +} + +void EndFullscreenWindow() +{ + ImGui::End(); + ImGui::PopStyleVar(3); + ImGui::PopStyleColor(); +} + +void BeginMenuButtons(u32 num_items, float y_align, float x_padding, float y_padding, float item_height) +{ + s_menu_button_index = 0; + + ImGui::PushStyleVar(ImGuiStyleVar_FramePadding, LayoutScale(x_padding, y_padding)); + ImGui::PushStyleVar(ImGuiStyleVar_FrameRounding, 0.0f); + ImGui::PushStyleVar(ImGuiStyleVar_FrameBorderSize, 1.0f); + ImGui::PushStyleVar(ImGuiStyleVar_ItemSpacing, ImVec2(0.0f, 0.0f)); + + if (y_align != 0.0f) + { + const float total_size = + static_cast(num_items) * LayoutScale(item_height + (y_padding * 2.0f)) + LayoutScale(y_padding * 2.0f); + const float window_height = ImGui::GetWindowHeight(); + if (window_height > total_size) + ImGui::SetCursorPosY((window_height - total_size) * y_align); + } +} + +void EndMenuButtons() +{ + ImGui::PopStyleVar(4); +} + +void DrawWindowTitle(const char* title) +{ + ImGuiWindow* window = ImGui::GetCurrentWindow(); + const ImVec2 pos(window->DC.CursorPos + LayoutScale(LAYOUT_MENU_BUTTON_X_PADDING, LAYOUT_MENU_BUTTON_Y_PADDING)); + const ImVec2 size(window->WorkRect.GetWidth() - (LayoutScale(LAYOUT_MENU_BUTTON_X_PADDING) * 2.0f), + g_large_font->FontSize + LayoutScale(LAYOUT_MENU_BUTTON_Y_PADDING) * 2.0f); + const ImRect rect(pos, pos + size); + + ImGui::ItemSize(size); + if (!ImGui::ItemAdd(rect, window->GetID("window_title"))) + return; + + ImGui::PushFont(g_large_font); + ImGui::RenderTextClipped(rect.Min, rect.Max, title, nullptr, nullptr, ImVec2(0.0f, 0.0f), &rect); + ImGui::PopFont(); + + const ImVec2 line_start(pos.x, pos.y + g_large_font->FontSize + LayoutScale(LAYOUT_MENU_BUTTON_Y_PADDING)); + const ImVec2 line_end(pos.x + size.x, line_start.y); + const float line_thickness = LayoutScale(1.0f); + ImDrawList* dl = ImGui::GetWindowDrawList(); + dl->AddLine(line_start, line_end, IM_COL32(255, 255, 255, 255), line_thickness); +} + +static void GetMenuButtonFrameBounds(float height, ImVec2* pos, ImVec2* size) +{ + ImGuiWindow* window = ImGui::GetCurrentWindow(); + *pos = window->DC.CursorPos; + *size = ImVec2(window->WorkRect.GetWidth(), LayoutScale(height) + ImGui::GetStyle().FramePadding.y * 2.0f); +} + +static bool MenuButtonFrame(const char* str_id, bool enabled, float height, bool* visible, bool* hovered, ImRect* bb, + ImGuiButtonFlags flags = 0) +{ + ImGuiWindow* window = ImGui::GetCurrentWindow(); + if (window->SkipItems) + return false; + + ImVec2 pos, size; + GetMenuButtonFrameBounds(height, &pos, &size); + *bb = ImRect(pos, pos + size); + + const ImGuiID id = window->GetID(str_id); + ImGui::ItemSize(size); + if (enabled) + { + if (!ImGui::ItemAdd(*bb, id)) + { + *visible = false; + *hovered = false; + return false; + } + } + else + { + if (ImGui::IsClippedEx(*bb, id, false)) + { + *visible = false; + *hovered = false; + return false; + } + } + + *visible = true; + + bool held; + bool pressed; + if (enabled) + { + pressed = ImGui::ButtonBehavior(*bb, id, hovered, &held, flags); + if (*hovered) + { + const ImU32 col = ImGui::GetColorU32(held ? ImGuiCol_ButtonActive : ImGuiCol_ButtonHovered); + ImGui::RenderFrame(bb->Min, bb->Max, col, true, 0.0f); + } + } + else + { + pressed = false; + held = false; + } + + const ImGuiStyle& style = ImGui::GetStyle(); + bb->Min += style.FramePadding; + bb->Max -= style.FramePadding; + + return pressed; +} + +bool MenuButtonFrame(const char* str_id, bool enabled, float height, bool* visible, bool* hovered, ImVec2* min, + ImVec2* max, ImGuiButtonFlags flags /*= 0*/) +{ + ImRect bb; + const bool result = MenuButtonFrame(str_id, enabled, height, visible, hovered, &bb, flags); + *min = bb.Min; + *max = bb.Max; + return result; +} + +void MenuHeading(const char* title, bool draw_line /*= true*/) +{ + const float line_thickness = draw_line ? LayoutScale(1.0f) : 0.0f; + const float line_padding = draw_line ? LayoutScale(5.0f) : 0.0f; + + bool visible, hovered; + ImRect bb; + MenuButtonFrame("menu_heading", false, LAYOUT_MENU_BUTTON_HEIGHT_NO_SUMMARY, &visible, &hovered, &bb); + if (!visible) + return; + + ImGui::PushStyleColor(ImGuiCol_Text, ImGui::GetColorU32(ImGuiCol_TextDisabled)); + ImGui::PushFont(g_large_font); + ImGui::RenderTextClipped(bb.Min, bb.Max, title, nullptr, nullptr, ImVec2(0.0f, 0.0f), &bb); + ImGui::PopFont(); + ImGui::PopStyleColor(); + + if (draw_line) + { + const ImVec2 line_start(bb.Min.x, bb.Min.y + g_large_font->FontSize + line_padding); + const ImVec2 line_end(bb.Max.x, line_start.y); + ImGui::GetWindowDrawList()->AddLine(line_start, line_end, ImGui::GetColorU32(ImGuiCol_TextDisabled)); + } +} + +bool ActiveButton(const char* title, bool is_active, bool enabled, float height, ImFont* font) +{ + if (is_active) + { + ImVec2 pos, size; + GetMenuButtonFrameBounds(height, &pos, &size); + ImGui::RenderFrame(pos, pos + size, ImGui::GetColorU32(UIPrimaryColor()), false); + } + + ImRect bb; + bool visible, hovered; + bool pressed = MenuButtonFrame(title, enabled, height, &visible, &hovered, &bb); + if (!visible) + return false; + + const ImRect title_bb(bb.GetTL(), bb.GetBR()); + + if (!enabled) + ImGui::PushStyleColor(ImGuiCol_Text, ImGui::GetColorU32(ImGuiCol_TextDisabled)); + + ImGui::PushFont(font); + ImGui::RenderTextClipped(title_bb.Min, title_bb.Max, title, nullptr, nullptr, ImVec2(0.0f, 0.0f), &title_bb); + ImGui::PopFont(); + + if (!enabled) + ImGui::PopStyleColor(); + + s_menu_button_index++; + return pressed; +} + +bool MenuButton(const char* title, const char* summary, bool enabled, float height, ImFont* font, ImFont* summary_font) +{ + ImRect bb; + bool visible, hovered; + bool pressed = MenuButtonFrame(title, enabled, height, &visible, &hovered, &bb); + if (!visible) + return false; + + const float midpoint = bb.Min.y + font->FontSize + LayoutScale(4.0f); + const ImRect title_bb(bb.Min, ImVec2(bb.Max.x, midpoint)); + const ImRect summary_bb(ImVec2(bb.Min.x, midpoint), bb.Max); + + if (!enabled) + ImGui::PushStyleColor(ImGuiCol_Text, ImGui::GetColorU32(ImGuiCol_TextDisabled)); + + ImGui::PushFont(font); + ImGui::RenderTextClipped(title_bb.Min, title_bb.Max, title, nullptr, nullptr, ImVec2(0.0f, 0.0f), &title_bb); + ImGui::PopFont(); + + if (summary) + { + ImGui::PushFont(summary_font); + ImGui::RenderTextClipped(summary_bb.Min, summary_bb.Max, summary, nullptr, nullptr, ImVec2(0.0f, 0.0f), + &summary_bb); + ImGui::PopFont(); + } + + if (!enabled) + ImGui::PopStyleColor(); + + s_menu_button_index++; + return pressed; +} + +bool MenuImageButton(const char* title, const char* summary, ImTextureID user_texture_id, const ImVec2& image_size, + bool enabled, float height, const ImVec2& uv0, const ImVec2& uv1, ImFont* title_font, + ImFont* summary_font) +{ + ImRect bb; + bool visible, hovered; + bool pressed = MenuButtonFrame(title, enabled, height, &visible, &hovered, &bb); + if (!visible) + return false; + + ImGui::GetWindowDrawList()->AddImage(user_texture_id, bb.Min, bb.Min + image_size, uv0, uv1, + enabled ? IM_COL32(255, 255, 255, 255) : + ImGui::GetColorU32(ImGuiCol_TextDisabled)); + + const float midpoint = bb.Min.y + title_font->FontSize + LayoutScale(4.0f); + const float text_start_x = bb.Min.x + image_size.x + LayoutScale(15.0f); + const ImRect title_bb(ImVec2(text_start_x, bb.Min.y), ImVec2(bb.Max.x, midpoint)); + const ImRect summary_bb(ImVec2(text_start_x, midpoint), bb.Max); + + if (!enabled) + ImGui::PushStyleColor(ImGuiCol_Text, ImGui::GetColorU32(ImGuiCol_TextDisabled)); + + ImGui::PushFont(title_font); + ImGui::RenderTextClipped(title_bb.Min, title_bb.Max, title, nullptr, nullptr, ImVec2(0.0f, 0.0f), &title_bb); + ImGui::PopFont(); + + if (summary) + { + ImGui::PushFont(summary_font); + ImGui::RenderTextClipped(summary_bb.Min, summary_bb.Max, summary, nullptr, nullptr, ImVec2(0.0f, 0.0f), + &summary_bb); + ImGui::PopFont(); + } + + if (!enabled) + ImGui::PopStyleColor(); + + s_menu_button_index++; + return pressed; +} + +bool ToggleButton(const char* title, const char* summary, bool* v, bool enabled, float height, ImFont* font, + ImFont* summary_font) +{ + ImRect bb; + bool visible, hovered; + bool pressed = MenuButtonFrame(title, enabled, height, &visible, &hovered, &bb, ImGuiButtonFlags_PressedOnClick); + if (!visible) + return false; + + const float midpoint = bb.Min.y + font->FontSize + LayoutScale(4.0f); + const ImRect title_bb(bb.Min, ImVec2(bb.Max.x, midpoint)); + const ImRect summary_bb(ImVec2(bb.Min.x, midpoint), bb.Max); + + if (!enabled) + ImGui::PushStyleColor(ImGuiCol_Text, ImGui::GetColorU32(ImGuiCol_TextDisabled)); + + ImGui::PushFont(font); + ImGui::RenderTextClipped(title_bb.Min, title_bb.Max, title, nullptr, nullptr, ImVec2(0.0f, 0.0f), &title_bb); + ImGui::PopFont(); + + if (summary) + { + ImGui::PushFont(summary_font); + ImGui::RenderTextClipped(summary_bb.Min, summary_bb.Max, summary, nullptr, nullptr, ImVec2(0.0f, 0.0f), + &summary_bb); + ImGui::PopFont(); + } + + if (!enabled) + ImGui::PopStyleColor(); + + const float toggle_width = LayoutScale(50.0f); + const float toggle_height = LayoutScale(25.0f); + const float toggle_x = LayoutScale(8.0f); + const float toggle_y = (LayoutScale(LAYOUT_MENU_BUTTON_HEIGHT) - toggle_height) * 0.5f; + const float toggle_radius = toggle_height * 0.5f; + const ImVec2 toggle_pos(bb.Max.x - toggle_width - toggle_x, bb.Min.y + toggle_y); + + if (pressed) + *v = !*v; + + float t = *v ? 1.0f : 0.0f; + ImDrawList* dl = ImGui::GetWindowDrawList(); + ImGuiContext& g = *GImGui; + float ANIM_SPEED = 0.08f; + if (g.LastActiveId == g.CurrentWindow->GetID(title)) // && g.LastActiveIdTimer < ANIM_SPEED) + { + float t_anim = ImSaturate(g.LastActiveIdTimer / ANIM_SPEED); + t = *v ? (t_anim) : (1.0f - t_anim); + } + + ImU32 col_bg; + if (!enabled) + col_bg = IM_COL32(0x75, 0x75, 0x75, 0xff); + else if (hovered) + col_bg = ImGui::GetColorU32(ImLerp(HEX_TO_IMVEC4(0x9e9e9e, 0xff), UISecondaryLightColor(), t)); + else + col_bg = ImGui::GetColorU32(ImLerp(HEX_TO_IMVEC4(0x757575, 0xff), UISecondaryLightColor(), t)); + + dl->AddRectFilled(toggle_pos, ImVec2(toggle_pos.x + toggle_width, toggle_pos.y + toggle_height), col_bg, + toggle_height * 0.5f); + dl->AddCircleFilled( + ImVec2(toggle_pos.x + toggle_radius + t * (toggle_width - toggle_radius * 2.0f), toggle_pos.y + toggle_radius), + toggle_radius - 1.5f, IM_COL32(255, 255, 255, 255), 32); + + s_menu_button_index++; + return pressed; +} + +bool RangeButton(const char* title, const char* summary, s32* value, s32 min, s32 max, s32 increment, + const char* format, bool enabled /*= true*/, float height /*= LAYOUT_MENU_BUTTON_HEIGHT*/, + ImFont* font /*= g_large_font*/, ImFont* summary_font /*= g_medium_font*/) +{ + ImRect bb; + bool visible, hovered; + bool pressed = MenuButtonFrame(title, enabled, height, &visible, &hovered, &bb); + if (!visible) + return false; + + TinyString value_text; + value_text.Format(format, *value); + const ImVec2 value_size(ImGui::CalcTextSize(value_text)); + + const float midpoint = bb.Min.y + font->FontSize + LayoutScale(4.0f); + const float text_end = bb.Max.x - value_size.x; + const ImRect title_bb(bb.Min, ImVec2(text_end, midpoint)); + const ImRect summary_bb(ImVec2(bb.Min.x, midpoint), ImVec2(text_end, bb.Max.y)); + + if (!enabled) + ImGui::PushStyleColor(ImGuiCol_Text, ImGui::GetColorU32(ImGuiCol_TextDisabled)); + + ImGui::PushFont(font); + ImGui::RenderTextClipped(title_bb.Min, title_bb.Max, title, nullptr, nullptr, ImVec2(0.0f, 0.0f), &title_bb); + ImGui::RenderTextClipped(bb.Min, bb.Max, value_text, nullptr, nullptr, ImVec2(1.0f, 0.5f), &bb); + ImGui::PopFont(); + + if (summary) + { + ImGui::PushFont(summary_font); + ImGui::RenderTextClipped(summary_bb.Min, summary_bb.Max, summary, nullptr, nullptr, ImVec2(0.0f, 0.0f), + &summary_bb); + ImGui::PopFont(); + } + + if (!enabled) + ImGui::PopStyleColor(); + + if (pressed) + ImGui::OpenPopup(title); + + bool changed = false; + + ImGui::SetNextWindowSize(LayoutScale(300.0f, 120.0f)); + + if (ImGui::BeginPopupModal(title, nullptr)) + { + ImGui::SetNextItemWidth(LayoutScale(300.0f)); + changed = ImGui::SliderInt("##value", value, min, max, format, ImGuiSliderFlags_NoInput); + + BeginMenuButtons(); + if (MenuButton("OK", nullptr, true, LAYOUT_MENU_BUTTON_HEIGHT_NO_SUMMARY)) + ImGui::CloseCurrentPopup(); + EndMenuButtons(); + + ImGui::EndPopup(); + } + + return changed; +} + +bool RangeButton(const char* title, const char* summary, float* value, float min, float max, float increment, + const char* format, bool enabled /*= true*/, float height /*= LAYOUT_MENU_BUTTON_HEIGHT*/, + ImFont* font /*= g_large_font*/, ImFont* summary_font /*= g_medium_font*/) +{ + ImRect bb; + bool visible, hovered; + bool pressed = MenuButtonFrame(title, enabled, height, &visible, &hovered, &bb); + if (!visible) + return false; + + TinyString value_text; + value_text.Format(format, *value); + const ImVec2 value_size(ImGui::CalcTextSize(value_text)); + + const float midpoint = bb.Min.y + font->FontSize + LayoutScale(4.0f); + const float text_end = bb.Max.x - value_size.x; + const ImRect title_bb(bb.Min, ImVec2(text_end, midpoint)); + const ImRect summary_bb(ImVec2(bb.Min.x, midpoint), ImVec2(text_end, bb.Max.y)); + + if (!enabled) + ImGui::PushStyleColor(ImGuiCol_Text, ImGui::GetColorU32(ImGuiCol_TextDisabled)); + + ImGui::PushFont(font); + ImGui::RenderTextClipped(title_bb.Min, title_bb.Max, title, nullptr, nullptr, ImVec2(0.0f, 0.0f), &title_bb); + ImGui::RenderTextClipped(bb.Min, bb.Max, value_text, nullptr, nullptr, ImVec2(1.0f, 0.5f), &bb); + ImGui::PopFont(); + + if (summary) + { + ImGui::PushFont(summary_font); + ImGui::RenderTextClipped(summary_bb.Min, summary_bb.Max, summary, nullptr, nullptr, ImVec2(0.0f, 0.0f), + &summary_bb); + ImGui::PopFont(); + } + + if (!enabled) + ImGui::PopStyleColor(); + + if (pressed) + ImGui::OpenPopup(title); + + bool changed = false; + + ImGui::SetNextWindowSize(LayoutScale(300.0f, 120.0f)); + + if (ImGui::BeginPopupModal(title, nullptr)) + { + ImGui::SetNextItemWidth(LayoutScale(300.0f)); + changed = ImGui::SliderFloat("##value", value, min, max, format, ImGuiSliderFlags_NoInput); + + BeginMenuButtons(); + if (MenuButton("OK", nullptr, true, LAYOUT_MENU_BUTTON_HEIGHT_NO_SUMMARY)) + ImGui::CloseCurrentPopup(); + EndMenuButtons(); + + ImGui::EndPopup(); + } + + return changed; +} + +bool MenuButtonWithValue(const char* title, const char* summary, const char* value, bool enabled, float height, + ImFont* font, ImFont* summary_font) +{ + ImRect bb; + bool visible, hovered; + bool pressed = MenuButtonFrame(title, enabled, height, &visible, &hovered, &bb); + if (!visible) + return false; + + const ImVec2 value_size(ImGui::CalcTextSize(value)); + + const float midpoint = bb.Min.y + font->FontSize + LayoutScale(4.0f); + const float text_end = bb.Max.x - value_size.x; + const ImRect title_bb(bb.Min, ImVec2(text_end, midpoint)); + const ImRect summary_bb(ImVec2(bb.Min.x, midpoint), ImVec2(text_end, bb.Max.y)); + + if (!enabled) + ImGui::PushStyleColor(ImGuiCol_Text, ImGui::GetColorU32(ImGuiCol_TextDisabled)); + + ImGui::PushFont(font); + ImGui::RenderTextClipped(title_bb.Min, title_bb.Max, title, nullptr, nullptr, ImVec2(0.0f, 0.0f), &title_bb); + ImGui::RenderTextClipped(bb.Min, bb.Max, value, nullptr, nullptr, ImVec2(1.0f, 0.5f), &bb); + ImGui::PopFont(); + + if (summary) + { + ImGui::PushFont(summary_font); + ImGui::RenderTextClipped(summary_bb.Min, summary_bb.Max, summary, nullptr, nullptr, ImVec2(0.0f, 0.0f), + &summary_bb); + ImGui::PopFont(); + } + + if (!enabled) + ImGui::PopStyleColor(); + + return pressed; +} + +static ImGuiID s_enum_choice_button_id = 0; +static s32 s_enum_choice_button_value = 0; +static bool s_enum_choice_button_set = false; + +bool EnumChoiceButtonImpl(const char* title, const char* summary, s32* value_pointer, + const char* (*to_display_name_function)(s32 value, void* opaque), void* opaque, u32 count, + bool enabled, float height, ImFont* font, ImFont* summary_font) +{ + const bool pressed = MenuButtonWithValue(title, summary, to_display_name_function(*value_pointer, opaque), enabled, + height, font, summary_font); + + if (pressed) + { + s_enum_choice_button_id = ImGui::GetID(title); + s_enum_choice_button_value = *value_pointer; + s_enum_choice_button_set = false; + + ChoiceDialogOptions options; + options.reserve(count); + for (u32 i = 0; i < count; i++) + options.emplace_back(to_display_name_function(static_cast(i), opaque), *value_pointer == i); + OpenChoiceDialog(title, false, std::move(options), [](s32 index, const std::string& title, bool checked) { + if (index >= 0) + s_enum_choice_button_value = index; + + s_enum_choice_button_set = true; + CloseChoiceDialog(); + }); + } + + bool changed = false; + if (s_enum_choice_button_set && s_enum_choice_button_id == ImGui::GetID(title)) + { + changed = s_enum_choice_button_value != *value_pointer; + if (changed) + *value_pointer = s_enum_choice_button_value; + + s_enum_choice_button_id = 0; + s_enum_choice_button_value = 0; + s_enum_choice_button_set = false; + } + + return changed; +} + +struct FileSelectorItem +{ + FileSelectorItem() = default; + FileSelectorItem(std::string display_name_, std::string full_path_, bool is_file_) + : display_name(std::move(display_name_)), full_path(std::move(full_path_)), is_file(is_file_) + { + } + FileSelectorItem(const FileSelectorItem&) = default; + FileSelectorItem(FileSelectorItem&&) = default; + ~FileSelectorItem() = default; + + FileSelectorItem& operator=(const FileSelectorItem&) = default; + FileSelectorItem& operator=(FileSelectorItem&&) = default; + + std::string display_name; + std::string full_path; + bool is_file; +}; + +static bool s_file_selector_open = false; +static bool s_file_selector_directory = false; +static std::string s_file_selector_title; +static FileSelectorCallback s_file_selector_callback; +static std::string s_file_selector_current_directory; +static std::vector s_file_selector_filters; +static std::vector s_file_selector_items; + +static void PopulateFileSelectorItems() +{ + s_file_selector_items.clear(); + + if (s_file_selector_current_directory.empty()) + { + for (std::string& root_path : FileSystem::GetRootDirectoryList()) + { + s_file_selector_items.emplace_back(StringUtil::StdStringFromFormat(ICON_FA_FOLDER " %s", root_path.c_str()), + std::move(root_path), false); + } + } + else + { + FileSystem::FindResultsArray results; + FileSystem::FindFiles(s_file_selector_current_directory.c_str(), "*", + FILESYSTEM_FIND_FILES | FILESYSTEM_FIND_FOLDERS | FILESYSTEM_FIND_HIDDEN_FILES | + FILESYSTEM_FIND_RELATIVE_PATHS, + &results); + + std::string parent_path; + std::string::size_type sep_pos = s_file_selector_current_directory.rfind(FS_OSPATH_SEPARATOR_CHARACTER); + if (sep_pos != std::string::npos) + { + parent_path = s_file_selector_current_directory.substr(0, sep_pos); + FileSystem::CanonicalizePath(parent_path, true); + } + + s_file_selector_items.emplace_back(ICON_FA_FOLDER_OPEN " ", std::move(parent_path), false); + std::sort(results.begin(), results.end(), [](const FILESYSTEM_FIND_DATA& lhs, const FILESYSTEM_FIND_DATA& rhs) { + if ((lhs.Attributes & FILESYSTEM_FILE_ATTRIBUTE_DIRECTORY) != + (rhs.Attributes & FILESYSTEM_FILE_ATTRIBUTE_DIRECTORY)) + return (lhs.Attributes & FILESYSTEM_FILE_ATTRIBUTE_DIRECTORY) != 0; + + // return std::lexicographical_compare(lhs.FileName.begin(), lhs.FileName.end(), rhs.FileName.begin(), + // rhs.FileName.end()); + return (StringUtil::Strcasecmp(lhs.FileName.c_str(), rhs.FileName.c_str()) < 0); + }); + + for (const FILESYSTEM_FIND_DATA& fd : results) + { + std::string full_path(StringUtil::StdStringFromFormat( + "%s" FS_OSPATH_SEPARATOR_STR "%s", s_file_selector_current_directory.c_str(), fd.FileName.c_str())); + + if (fd.Attributes & FILESYSTEM_FILE_ATTRIBUTE_DIRECTORY) + { + std::string title(StringUtil::StdStringFromFormat(ICON_FA_FOLDER " %s", fd.FileName.c_str())); + s_file_selector_items.emplace_back(std::move(title), std::move(full_path), false); + } + else + { + if (s_file_selector_filters.empty() || + std::none_of(s_file_selector_filters.begin(), s_file_selector_filters.end(), + [&fd](const std::string& filter) { + return StringUtil::WildcardMatch(fd.FileName.c_str(), filter.c_str()); + })) + { + continue; + } + + std::string title(StringUtil::StdStringFromFormat(ICON_FA_FILE " %s", fd.FileName.c_str())); + s_file_selector_items.emplace_back(std::move(title), std::move(full_path), true); + } + } + } +} + +static void SetFileSelectorDirectory(std::string dir) +{ + while (!dir.empty() && dir.back() == FS_OSPATH_SEPARATOR_CHARACTER) + dir.erase(dir.size() - 1); + + s_file_selector_current_directory = std::move(dir); + PopulateFileSelectorItems(); +} + +void OpenFileSelector(const char* title, bool select_directory, FileSelectorCallback callback, + FileSelectorFilters filters, std::string initial_directory) +{ + if (s_file_selector_open) + CloseFileSelector(); + + s_file_selector_open = true; + s_file_selector_directory = select_directory; + s_file_selector_title = StringUtil::StdStringFromFormat("%s##file_selector", title); + s_file_selector_callback = std::move(callback); + s_file_selector_filters = std::move(filters); + + if (initial_directory.empty() || !FileSystem::DirectoryExists(initial_directory.c_str())) + initial_directory = FileSystem::GetWorkingDirectory(); + SetFileSelectorDirectory(std::move(initial_directory)); +} + +void CloseFileSelector() +{ + if (!s_file_selector_open) + return; + + s_file_selector_open = false; + s_file_selector_directory = false; + std::string().swap(s_file_selector_title); + FileSelectorCallback().swap(s_file_selector_callback); + FileSelectorFilters().swap(s_file_selector_filters); + std::string().swap(s_file_selector_current_directory); + s_file_selector_items.clear(); + ImGui::CloseCurrentPopup(); +} + +void DrawFileSelector() +{ + if (!s_file_selector_open) + return; + + ImGui::SetNextWindowSize(LayoutScale(1000.0f, 680.0f)); + ImGui::SetNextWindowPos(ImGui::GetIO().DisplaySize * 0.5f, ImGuiCond_Always, ImVec2(0.5f, 0.5f)); + ImGui::OpenPopup(s_file_selector_title.c_str()); + + FileSelectorItem* selected = nullptr; + + ImGui::PushFont(g_large_font); + ImGui::PushStyleVar(ImGuiStyleVar_WindowRounding, LayoutScale(10.0f)); + ImGui::PushStyleVar(ImGuiStyleVar_FramePadding, + LayoutScale(LAYOUT_MENU_BUTTON_X_PADDING, LAYOUT_MENU_BUTTON_Y_PADDING)); + + bool is_open = (ImGui::GetNavInputAmount(ImGuiNavInput_Cancel, ImGuiInputReadMode_Pressed) < 1.0f); + bool directory_selected = false; + if (ImGui::BeginPopupModal(s_file_selector_title.c_str(), &is_open, + ImGuiWindowFlags_NoCollapse | ImGuiWindowFlags_NoResize | ImGuiWindowFlags_NoMove)) + { + BeginMenuButtons(); + + if (!s_file_selector_current_directory.empty()) + { + MenuButton(TinyString::FromFormat(ICON_FA_FOLDER_OPEN " %s", s_file_selector_current_directory.c_str()), nullptr, + false, LAYOUT_MENU_BUTTON_HEIGHT_NO_SUMMARY); + } + + if (s_file_selector_directory && !s_file_selector_current_directory.empty()) + { + if (MenuButton(ICON_FA_FOLDER_PLUS " ", nullptr, true, LAYOUT_MENU_BUTTON_HEIGHT_NO_SUMMARY)) + directory_selected = true; + } + + SmallString title; + for (FileSelectorItem& item : s_file_selector_items) + { + if (MenuButton(item.display_name.c_str(), nullptr, true, LAYOUT_MENU_BUTTON_HEIGHT_NO_SUMMARY)) + selected = &item; + } + + EndMenuButtons(); + + ImGui::EndPopup(); + } + else + { + is_open = false; + } + + ImGui::PopStyleVar(2); + ImGui::PopFont(); + + if (selected) + { + if (selected->is_file) + { + s_file_selector_callback(selected->full_path); + } + else + { + SetFileSelectorDirectory(std::move(selected->full_path)); + } + } + else if (directory_selected) + { + s_file_selector_callback(s_file_selector_current_directory); + } + else if (!is_open) + { + std::string no_path; + s_file_selector_callback(no_path); + CloseFileSelector(); + } +} + +static bool s_choice_dialog_open = false; +static bool s_choice_dialog_checkable = false; +static std::string s_choice_dialog_title; +static ChoiceDialogOptions s_choice_dialog_options; +static ChoiceDialogCallback s_choice_dialog_callback; + +void OpenChoiceDialog(const char* title, bool checkable, ChoiceDialogOptions options, ChoiceDialogCallback callback) +{ + if (s_choice_dialog_open) + CloseChoiceDialog(); + + s_choice_dialog_open = true; + s_choice_dialog_checkable = checkable; + s_choice_dialog_title = StringUtil::StdStringFromFormat("%s##choice_dialog", title); + s_choice_dialog_options = std::move(options); + s_choice_dialog_callback = std::move(callback); +} + +void CloseChoiceDialog() +{ + if (!s_choice_dialog_open) + return; + + s_choice_dialog_open = false; + s_choice_dialog_checkable = false; + std::string().swap(s_choice_dialog_title); + ChoiceDialogOptions().swap(s_choice_dialog_options); + ChoiceDialogCallback().swap(s_choice_dialog_callback); +} + +void DrawChoiceDialog() +{ + if (!s_choice_dialog_open) + return; + + const float width = 600.0f; + const float title_height = + g_large_font->FontSize + ImGui::GetStyle().FramePadding.y * 2.0f + ImGui::GetStyle().WindowPadding.y * 2.0f; + const float height = + std::min(400.0f, title_height + (LAYOUT_MENU_BUTTON_HEIGHT_NO_SUMMARY + (LAYOUT_MENU_BUTTON_Y_PADDING * 2.0f)) * + static_cast(s_choice_dialog_options.size())); + ImGui::SetNextWindowSize(LayoutScale(width, height)); + ImGui::SetNextWindowPos(ImGui::GetIO().DisplaySize * 0.5f, ImGuiCond_Always, ImVec2(0.5f, 0.5f)); + ImGui::OpenPopup(s_choice_dialog_title.c_str()); + + ImGui::PushFont(g_large_font); + ImGui::PushStyleVar(ImGuiStyleVar_WindowRounding, LayoutScale(10.0f)); + ImGui::PushStyleVar(ImGuiStyleVar_FramePadding, + LayoutScale(LAYOUT_MENU_BUTTON_X_PADDING, LAYOUT_MENU_BUTTON_Y_PADDING)); + + bool is_open = (ImGui::GetNavInputAmount(ImGuiNavInput_Cancel, ImGuiInputReadMode_Pressed) < 1.0f); + s32 choice = -1; + bool choice_checked = false; + + if (ImGui::BeginPopupModal(s_choice_dialog_title.c_str(), &is_open, + ImGuiWindowFlags_NoCollapse | ImGuiWindowFlags_NoResize | ImGuiWindowFlags_NoMove)) + { + BeginMenuButtons(); + + if (s_choice_dialog_checkable) + { + SmallString title; + for (s32 i = 0; i < static_cast(s_choice_dialog_options.size()); i++) + { + auto& option = s_choice_dialog_options[i]; + + title.Format("%s %s", option.second ? ICON_FA_CHECK_SQUARE : ICON_FA_SQUARE, option.first.c_str()); + if (MenuButton(title, nullptr, true, LAYOUT_MENU_BUTTON_HEIGHT_NO_SUMMARY)) + { + choice = i; + option.second = !option.second; + } + } + } + else + { + for (s32 i = 0; i < static_cast(s_choice_dialog_options.size()); i++) + { + auto& option = s_choice_dialog_options[i]; + SmallString title; + if (option.second) + title.AppendString(ICON_FA_CHECK " "); + title.AppendString(option.first); + + if (ActiveButton(title, option.second, true, LAYOUT_MENU_BUTTON_HEIGHT_NO_SUMMARY)) + { + choice = i; + for (s32 j = 0; j < static_cast(s_choice_dialog_options.size()); j++) + s_choice_dialog_options[j].second = (j == i); + } + } + } + + EndMenuButtons(); + + ImGui::EndPopup(); + } + else + { + is_open = false; + } + + ImGui::PopStyleVar(2); + ImGui::PopFont(); + + if (choice >= 0) + { + const auto& option = s_choice_dialog_options[choice]; + s_choice_dialog_callback(choice, option.first, option.second); + } + else if (!is_open) + { + std::string no_string; + s_choice_dialog_callback(-1, no_string, false); + CloseChoiceDialog(); + } +} + +struct BackgroundProgressDialogData +{ + std::string message; + ImGuiID id; + s32 min; + s32 max; + s32 value; +}; + +static std::vector s_background_progress_dialogs; +static std::mutex s_background_progress_lock; + +static ImGuiID GetBackgroundProgressID(const char* str_id) +{ + return ImHashStr(str_id); +} + +void OpenBackgroundProgressDialog(const char* str_id, std::string message, s32 min, s32 max, s32 value) +{ + const ImGuiID id = GetBackgroundProgressID(str_id); + + std::unique_lock lock(s_background_progress_lock); + + for (const BackgroundProgressDialogData& data : s_background_progress_dialogs) + { + Assert(data.id != id); + } + + BackgroundProgressDialogData data; + data.id = id; + data.message = std::move(message); + data.min = min; + data.max = max; + data.value = value; + s_background_progress_dialogs.push_back(std::move(data)); +} + +void UpdateBackgroundProgressDialog(const char* str_id, std::string message, s32 min, s32 max, s32 value) +{ + const ImGuiID id = GetBackgroundProgressID(str_id); + + std::unique_lock lock(s_background_progress_lock); + + for (BackgroundProgressDialogData& data : s_background_progress_dialogs) + { + if (data.id == id) + { + data.message = std::move(message); + data.min = min; + data.max = max; + data.value = value; + return; + } + } + + Panic("Updating unknown progress entry."); +} + +void CloseBackgroundProgressDialog(const char* str_id) +{ + const ImGuiID id = GetBackgroundProgressID(str_id); + + std::unique_lock lock(s_background_progress_lock); + + for (auto it = s_background_progress_dialogs.begin(); it != s_background_progress_dialogs.end(); ++it) + { + if (it->id == id) + { + s_background_progress_dialogs.erase(it); + return; + } + } + + Panic("Closing unknown progress entry."); +} + +void DrawBackgroundProgressDialogs() +{ + std::unique_lock lock(s_background_progress_lock); + if (s_background_progress_dialogs.empty()) + return; + + const float window_width = LayoutScale(500.0f); + const float window_height = LayoutScale(75.0f); + const float window_spacing = LayoutScale(20.0f); + + float current_pos_x = LayoutScale(10.0f); + float current_pos_y = (LayoutScale(LAYOUT_SCREEN_HEIGHT) + g_layout_padding_top) - window_height - LayoutScale(10.0f); + + ImGui::PushStyleColor(ImGuiCol_WindowBg, UIPrimaryDarkColor()); + ImGui::PushStyleColor(ImGuiCol_PlotHistogram, UISecondaryLightColor()); + ImGui::PushStyleVar(ImGuiStyleVar_PopupRounding, LayoutScale(4.0f)); + ImGui::PushStyleVar(ImGuiStyleVar_PopupBorderSize, LayoutScale(1.0f)); + ImGui::PushStyleVar(ImGuiStyleVar_WindowPadding, LayoutScale(10.0f, 10.0f)); + ImGui::PushStyleVar(ImGuiStyleVar_ItemSpacing, LayoutScale(10.0f, 10.0f)); + ImGui::PushFont(g_medium_font); + + ImDrawList* dl = ImGui::GetForegroundDrawList(); + + for (const BackgroundProgressDialogData& data : s_background_progress_dialogs) + { + dl->AddRectFilled(ImVec2(current_pos_x, current_pos_y), + ImVec2(current_pos_x + window_width, current_pos_y + window_height), + IM_COL32(0x11, 0x11, 0x11, 200), LayoutScale(10.0f)); + + ImVec2 pos(current_pos_x + LayoutScale(10.0f), current_pos_y + LayoutScale(10.0f)); + dl->AddText(g_medium_font, g_medium_font->FontSize, pos, IM_COL32(255, 255, 255, 255), data.message.c_str(), + nullptr, 0.0f); + pos.y += g_medium_font->FontSize + LayoutScale(10.0f); + + ImVec2 bar_end(pos.x + window_width - LayoutScale(10.0f * 2.0f), pos.y + LayoutScale(25.0f)); + float fraction = static_cast(data.value - data.min) / static_cast(data.max - data.min); + dl->AddRectFilled(pos, bar_end, ImGui::GetColorU32(UIPrimaryDarkColor())); + dl->AddRectFilled(pos, ImVec2(pos.x + fraction * (bar_end.x - pos.x), bar_end.y), + ImGui::GetColorU32(UISecondaryColor())); + + TinyString text; + text.Format("%d%%", static_cast(std::round(fraction * 100.0f))); + const ImVec2 text_size(ImGui::CalcTextSize(text)); + const ImVec2 text_pos(pos.x + ((bar_end.x - pos.x) / 2.0f) - (text_size.x / 2.0f), + pos.y + ((bar_end.y - pos.y) / 2.0f) - (text_size.y / 2.0f)); + dl->AddText(g_medium_font, g_medium_font->FontSize, text_pos, ImGui::GetColorU32(UIPrimaryTextColor()), text); + + current_pos_y -= window_height - window_spacing; + } + + ImGui::PopFont(); + ImGui::PopStyleVar(4); + ImGui::PopStyleColor(2); +} + +} // namespace ImGuiFullscreen \ No newline at end of file diff --git a/src/frontend-common/imgui_fullscreen.h b/src/frontend-common/imgui_fullscreen.h new file mode 100644 index 000000000..486e03e03 --- /dev/null +++ b/src/frontend-common/imgui_fullscreen.h @@ -0,0 +1,226 @@ +#pragma once +#include "common/types.h" +#include "imgui.h" +#include +#include + +namespace ImGuiFullscreen { +#define HEX_TO_IMVEC4(hex, alpha) \ + ImVec4(static_cast((hex >> 16) & 0xFFu) / 255.0f, static_cast((hex >> 8) & 0xFFu) / 255.0f, \ + static_cast(hex & 0xFFu) / 255.0f, static_cast(alpha) / 255.0f) + +static constexpr float LAYOUT_SCREEN_WIDTH = 1280.0f; +static constexpr float LAYOUT_SCREEN_HEIGHT = 720.0f; +static constexpr float LAYOUT_LARGE_FONT_SIZE = 26.0f; +static constexpr float LAYOUT_MEDIUM_FONT_SIZE = 16.0f; +static constexpr float LAYOUT_SMALL_FONT_SIZE = 10.0f; +static constexpr float LAYOUT_MENU_BUTTON_HEIGHT = 50.0f; +static constexpr float LAYOUT_MENU_BUTTON_HEIGHT_NO_SUMMARY = 26.0f; +static constexpr float LAYOUT_MENU_BUTTON_X_PADDING = 15.0f; +static constexpr float LAYOUT_MENU_BUTTON_Y_PADDING = 10.0f; + +extern ImFont* g_standard_font; +extern ImFont* g_medium_font; +extern ImFont* g_large_font; + +extern float g_layout_scale; +extern float g_layout_padding_left; +extern float g_layout_padding_top; + +static ALWAYS_INLINE float DPIScale(float v) +{ + return ImGui::GetIO().DisplayFramebufferScale.x * v; +} + +static ALWAYS_INLINE float DPIScale(int v) +{ + return ImGui::GetIO().DisplayFramebufferScale.x * static_cast(v); +} + +static ALWAYS_INLINE ImVec2 DPIScale(const ImVec2& v) +{ + const ImVec2& fbs = ImGui::GetIO().DisplayFramebufferScale; + return ImVec2(v.x * fbs.x, v.y * fbs.y); +} + +static ALWAYS_INLINE float WindowWidthScale(float v) +{ + return ImGui::GetWindowWidth() * v; +} + +static ALWAYS_INLINE float WindowHeightScale(float v) +{ + return ImGui::GetWindowHeight() * v; +} + +static ALWAYS_INLINE float LayoutScale(float v) +{ + return g_layout_scale * v; +} + +static ALWAYS_INLINE ImVec2 LayoutScale(const ImVec2& v) +{ + return ImVec2(v.x * g_layout_scale, v.y * g_layout_scale); +} + +static ALWAYS_INLINE ImVec2 LayoutScale(float x, float y) +{ + return ImVec2(x * g_layout_scale, y * g_layout_scale); +} + +static ALWAYS_INLINE ImVec4 UIPrimaryColor() +{ + return HEX_TO_IMVEC4(0x212121, 0xff); +} + +static ALWAYS_INLINE ImVec4 UIPrimaryLightColor() +{ + return HEX_TO_IMVEC4(0x484848, 0xff); +} + +static ALWAYS_INLINE ImVec4 UIPrimaryDarkColor() +{ + return HEX_TO_IMVEC4(0x484848, 0xff); +} + +static ALWAYS_INLINE ImVec4 UIPrimaryTextColor() +{ + return HEX_TO_IMVEC4(0xffffff, 0xff); +} + +static ALWAYS_INLINE ImVec4 UIPrimaryDisabledTextColor() +{ + return HEX_TO_IMVEC4(0xaaaaaa, 0xff); +} + +static ALWAYS_INLINE ImVec4 UITextHighlightColor() +{ + return HEX_TO_IMVEC4(0x90caf9, 0xff); +} + +static ALWAYS_INLINE ImVec4 UIPrimaryLineColor() +{ + return HEX_TO_IMVEC4(0xffffff, 0xff); +} + +static ALWAYS_INLINE ImVec4 UISecondaryColor() +{ + return HEX_TO_IMVEC4(0x1565c0, 0xff); +} + +static ALWAYS_INLINE ImVec4 UISecondaryLightColor() +{ + return HEX_TO_IMVEC4(0x5e92f3, 0xff); +} + +static ALWAYS_INLINE ImVec4 UISecondaryDarkColor() +{ + return HEX_TO_IMVEC4(0x003c8f, 0xff); +} + +static ALWAYS_INLINE ImVec4 UISecondaryTextColor() +{ + return HEX_TO_IMVEC4(0xffffff, 0xff); +} + +void SetFontFilename(std::string filename); +void SetIconFontFilename(std::string icon_font_filename); +void SetIconFontData(std::vector data); +void SetFontSize(float size_pixels); +void SetFontGlyphRanges(const ImWchar* glyph_ranges); + +/// Changes the menu bar size. Don't forget to call UpdateLayoutScale() and UpdateFonts(). +void SetMenuBarSize(float size); + +/// Rebuilds fonts to a new scale if needed. Returns true if fonts have changed and the texture needs updating. +bool UpdateFonts(); + +bool UpdateLayoutScale(); + +void BeginLayout(); +void EndLayout(); + +void DrawWindowTitle(const char* title); + +bool BeginFullscreenColumns(const char* title = nullptr); +void EndFullscreenColumns(); + +bool BeginFullscreenColumnWindow(float start, float end, const char* name, + const ImVec4& background = HEX_TO_IMVEC4(0x212121, 0xFF)); +void EndFullscreenColumnWindow(); + +bool BeginFullscreenWindow(float left, float top, float width, float height, const char* name, + const ImVec4& background = HEX_TO_IMVEC4(0x212121, 0xFF), float rounding = 0.0f, + float padding = 0.0f, ImGuiWindowFlags flags = 0); +void EndFullscreenWindow(); + +void BeginMenuButtons(u32 num_items = 0, float y_align = 0.0f, float x_padding = LAYOUT_MENU_BUTTON_X_PADDING, + float y_padding = LAYOUT_MENU_BUTTON_Y_PADDING, float item_height = LAYOUT_MENU_BUTTON_HEIGHT); +void EndMenuButtons(); +bool MenuButtonFrame(const char* str_id, bool enabled, float height, bool* visible, bool* hovered, ImVec2* min, + ImVec2* max, ImGuiButtonFlags flags = 0); +void MenuHeading(const char* title, bool draw_line = true); +bool ActiveButton(const char* title, bool is_active, bool enabled = true, + float height = LAYOUT_MENU_BUTTON_HEIGHT_NO_SUMMARY, ImFont* font = g_large_font); +bool MenuButton(const char* title, const char* summary, bool enabled = true, float height = LAYOUT_MENU_BUTTON_HEIGHT, + ImFont* font = g_large_font, ImFont* summary_font = g_medium_font); +bool MenuButtonWithValue(const char* title, const char* summary, const char* value, bool enabled = true, + float height = LAYOUT_MENU_BUTTON_HEIGHT, ImFont* font = g_large_font, + ImFont* summary_font = g_medium_font); +bool MenuImageButton(const char* title, const char* summary, ImTextureID user_texture_id, const ImVec2& image_size, + bool enabled = true, float height = LAYOUT_MENU_BUTTON_HEIGHT, + const ImVec2& uv0 = ImVec2(0.0f, 0.0f), const ImVec2& uv1 = ImVec2(1.0f, 1.0f), + ImFont* font = g_large_font, ImFont* summary_font = g_medium_font); +bool ToggleButton(const char* title, const char* summary, bool* v, bool enabled = true, + float height = LAYOUT_MENU_BUTTON_HEIGHT, ImFont* font = g_large_font, + ImFont* summary_font = g_medium_font); +bool RangeButton(const char* title, const char* summary, s32* value, s32 min, s32 max, s32 increment, + const char* format = "%d", bool enabled = true, float height = LAYOUT_MENU_BUTTON_HEIGHT, + ImFont* font = g_large_font, ImFont* summary_font = g_medium_font); +bool RangeButton(const char* title, const char* summary, float* value, float min, float max, float increment, + const char* format = "%f", bool enabled = true, float height = LAYOUT_MENU_BUTTON_HEIGHT, + ImFont* font = g_large_font, ImFont* summary_font = g_medium_font); +bool EnumChoiceButtonImpl(const char* title, const char* summary, s32* value_pointer, + const char* (*to_display_name_function)(s32 value, void* opaque), void* opaque, u32 count, + bool enabled, float height, ImFont* font, ImFont* summary_font); + +template +static ALWAYS_INLINE bool EnumChoiceButton(const char* title, const char* summary, DataType* value_pointer, + const char* (*to_display_name_function)(DataType value), CountType count, + bool enabled = true, float height = LAYOUT_MENU_BUTTON_HEIGHT, + ImFont* font = g_large_font, ImFont* summary_font = g_medium_font) +{ + s32 value = static_cast(*value_pointer); + auto to_display_name_wrapper = [](s32 value, void* opaque) -> const char* { + return (*static_cast(opaque))(static_cast(value)); + }; + + if (EnumChoiceButtonImpl(title, summary, &value, to_display_name_wrapper, &to_display_name_function, + static_cast(count), enabled, height, font, summary_font)) + { + *value_pointer = static_cast(value); + return true; + } + else + { + return false; + } +} + +using FileSelectorCallback = std::function; +using FileSelectorFilters = std::vector; +void OpenFileSelector(const char* title, bool select_directory, FileSelectorCallback callback, + FileSelectorFilters filters = FileSelectorFilters(), + std::string initial_directory = std::string()); +void CloseFileSelector(); + +using ChoiceDialogCallback = std::function; +using ChoiceDialogOptions = std::vector>; +void OpenChoiceDialog(const char* title, bool checkable, ChoiceDialogOptions options, ChoiceDialogCallback callback); +void CloseChoiceDialog(); + +void OpenBackgroundProgressDialog(const char* str_id, std::string message, s32 min, s32 max, s32 value); +void UpdateBackgroundProgressDialog(const char* str_id, std::string message, s32 min, s32 max, s32 value); +void CloseBackgroundProgressDialog(const char* str_id); + +} // namespace ImGuiFullscreen