From be224a455682825762234130538f9fbcc6c3035f Mon Sep 17 00:00:00 2001 From: rainmakerv2 <30595646+rainmakerv3@users.noreply.github.com> Date: Wed, 15 Apr 2026 23:19:22 +0800 Subject: [PATCH] Big Picture: add settings configuration (#4255) * prototype settings look * put categories on top instead of on the side, plays nicer with input system * added profiles + saving and loading framework * add helper functions * fix autofocusing * add code for using embedded image files * put settings in different files * clang * remove sdl_image submodule, unify functions when possible * Always display selected profile in settings content areas * Fix focusing, logs, highlight profile text * persist highlight on focused buttons, add all categories placeholders * All settings added * navigate tabs with l1/r1 --- .gitmodules | 3 - CMakeLists.txt | 13 +- REUSE.toml | 8 + externals/CMakeLists.txt | 23 - externals/sdl3_image | 1 - src/core/devtools/layer.cpp | 4 +- src/core/emulator_settings.h | 1 - src/images/big_picture/controller.png | Bin 0 -> 2295 bytes src/images/big_picture/experimental.png | Bin 0 -> 4299 bytes src/images/big_picture/folder.png | Bin 0 -> 2838 bytes src/images/big_picture/global-settings.png | Bin 0 -> 14717 bytes src/images/big_picture/graphics.png | Bin 0 -> 993 bytes src/images/big_picture/log.png | Bin 0 -> 2173 bytes src/images/big_picture/settings.png | Bin 0 -> 2838 bytes src/images/big_picture/trophy.png | Bin 0 -> 2643 bytes src/imgui/big_picture.cpp | 174 ++++-- src/imgui/big_picture.h | 12 +- src/imgui/settings_dialog_imgui.cpp | 602 +++++++++++++++++++++ src/imgui/settings_dialog_imgui.h | 191 +++++++ 19 files changed, 953 insertions(+), 79 deletions(-) delete mode 160000 externals/sdl3_image create mode 100644 src/images/big_picture/controller.png create mode 100644 src/images/big_picture/experimental.png create mode 100644 src/images/big_picture/folder.png create mode 100644 src/images/big_picture/global-settings.png create mode 100644 src/images/big_picture/graphics.png create mode 100644 src/images/big_picture/log.png create mode 100644 src/images/big_picture/settings.png create mode 100644 src/images/big_picture/trophy.png create mode 100644 src/imgui/settings_dialog_imgui.cpp create mode 100644 src/imgui/settings_dialog_imgui.h diff --git a/.gitmodules b/.gitmodules index 660bb3046..042e83586 100644 --- a/.gitmodules +++ b/.gitmodules @@ -125,9 +125,6 @@ [submodule "externals/openal-soft"] path = externals/openal-soft url = https://github.com/shadexternals/openal-soft.git -[submodule "externals/sdl3_image"] - path = externals/sdl3_image - url = https://github.com/libsdl-org/SDL_image [submodule "externals/libusb"] path = externals/libusb url = https://github.com/shadexternals/libusb.git diff --git a/CMakeLists.txt b/CMakeLists.txt index 5f29e03a7..cd13a96c6 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -233,7 +233,6 @@ find_package(nlohmann_json 3.12 CONFIG) find_package(PNG 1.6 MODULE) find_package(OpenAL CONFIG) find_package(RenderDoc 1.6.0 MODULE) -find_package(SDL3_image CONFIG) find_package(SDL3 3.1.2 CONFIG) find_package(stb MODULE) find_package(toml11 4.2.0 CONFIG) @@ -1107,6 +1106,8 @@ set(IMGUI src/imgui/imgui_config.h src/imgui/renderer/texture_manager.h src/imgui/big_picture.cpp src/imgui/big_picture.h + src/imgui/settings_dialog_imgui.cpp + src/imgui/settings_dialog_imgui.h ) set(INPUT src/input/controller.cpp @@ -1144,7 +1145,7 @@ add_executable(shadps4 create_target_directory_groups(shadps4) target_link_libraries(shadps4 PRIVATE magic_enum::magic_enum fmt::fmt toml11::toml11 tsl::robin_map xbyak::xbyak Tracy::TracyClient RenderDoc::API FFmpeg::ffmpeg Dear_ImGui gcn half::half ZLIB::ZLIB PNG::PNG minimp3) -target_link_libraries(shadps4 PRIVATE Boost::headers GPUOpen::VulkanMemoryAllocator LibAtrac9 sirit Vulkan::Headers xxHash::xxhash Zydis::Zydis glslang::glslang SDL3::SDL3 SDL3_image::SDL3_image pugixml::pugixml) +target_link_libraries(shadps4 PRIVATE Boost::headers GPUOpen::VulkanMemoryAllocator LibAtrac9 sirit Vulkan::Headers xxHash::xxhash Zydis::Zydis glslang::glslang SDL3::SDL3 pugixml::pugixml) target_link_libraries(shadps4 PRIVATE stb::headers lfreist-hwinfo::hwinfo nlohmann_json::nlohmann_json miniz::miniz fdk-aac CLI11::CLI11 OpenAL::OpenAL Cpp_Httplib) if (${CMAKE_SYSTEM_NAME} STREQUAL "FreeBSD") @@ -1267,6 +1268,14 @@ include("${CMAKE_CURRENT_SOURCE_DIR}/cmake/CMakeRC.cmake") cmrc_add_resource_library(embedded-resources ALIAS res::embedded NAMESPACE res + src/images/big_picture/folder.png + src/images/big_picture/settings.png + src/images/big_picture/global-settings.png + src/images/big_picture/experimental.png + src/images/big_picture/graphics.png + src/images/big_picture/controller.png + src/images/big_picture/trophy.png + src/images/big_picture/log.png src/images/trophy.wav src/images/bronze.png src/images/gold.png diff --git a/REUSE.toml b/REUSE.toml index e8997f007..0d1505bab 100644 --- a/REUSE.toml +++ b/REUSE.toml @@ -76,6 +76,14 @@ path = [ "src/images/trophy.wav", "src/images/hotkey.png", "src/images/game_settings.png", + "src/images/big_picture/controller.png", + "src/images/big_picture/experimental.png", + "src/images/big_picture/folder.png", + "src/images/big_picture/global-settings.png", + "src/images/big_picture/graphics.png", + "src/images/big_picture/log.png", + "src/images/big_picture/settings.png", + "src/images/big_picture/trophy.png", "src/shadps4.rc", ] precedence = "aggregate" diff --git a/externals/CMakeLists.txt b/externals/CMakeLists.txt index 735552485..187c2f435 100644 --- a/externals/CMakeLists.txt +++ b/externals/CMakeLists.txt @@ -63,29 +63,6 @@ if (NOT TARGET SDL3::SDL3) add_subdirectory(sdl3) endif() -# SDL3_image -if (NOT TARGET SDL3_image::SDL3_image) - set(SDLIMAGE_VENDORED OFF CACHE BOOL "" FORCE) - set(SDLIMAGE_ANI OFF CACHE BOOL "" FORCE) - set(SDLIMAGE_AVIF OFF CACHE BOOL "" FORCE) - set(SDLIMAGE_BMP OFF CACHE BOOL "" FORCE) - set(SDLIMAGE_GIF OFF CACHE BOOL "" FORCE) - set(SDLIMAGE_JPG OFF CACHE BOOL "" FORCE) - set(SDLIMAGE_JXL OFF CACHE BOOL "" FORCE) - set(SDLIMAGE_LBM OFF CACHE BOOL "" FORCE) - set(SDLIMAGE_PCX OFF CACHE BOOL "" FORCE) - set(SDLIMAGE_PNM OFF CACHE BOOL "" FORCE) - set(SDLIMAGE_QOI OFF CACHE BOOL "" FORCE) - set(SDLIMAGE_SVG OFF CACHE BOOL "" FORCE) - set(SDLIMAGE_TGA OFF CACHE BOOL "" FORCE) - set(SDLIMAGE_TIF OFF CACHE BOOL "" FORCE) - set(SDLIMAGE_WEBP OFF CACHE BOOL "" FORCE) - set(SDLIMAGE_XCF OFF CACHE BOOL "" FORCE) - set(SDLIMAGE_XPM OFF CACHE BOOL "" FORCE) - set(SDLIMAGE_XV OFF CACHE BOOL "" FORCE) - add_subdirectory(sdl3_image) -endif() - # vulkan-headers if (NOT TARGET Vulkan::Headers) set(VULKAN_HEADERS_ENABLE_MODULE OFF) diff --git a/externals/sdl3_image b/externals/sdl3_image deleted file mode 160000 index 1aedddcbd..000000000 --- a/externals/sdl3_image +++ /dev/null @@ -1 +0,0 @@ -Subproject commit 1aedddcbd205c4e1ea0f99fdb2c785acc8e2489b diff --git a/src/core/devtools/layer.cpp b/src/core/devtools/layer.cpp index db28e2ec1..30fc5d6c1 100644 --- a/src/core/devtools/layer.cpp +++ b/src/core/devtools/layer.cpp @@ -484,11 +484,11 @@ void L::Draw() { namespace Overlay { void TextCentered(const std::string& text) { - float window_width = GetWindowSize().x; + float window_width = GetContentRegionAvail().x; float text_width = CalcTextSize(text.c_str()).x; float text_indentation = (window_width - text_width) * 0.5f; - SameLine(text_indentation); + SetCursorPosX(text_indentation); Text("%s", text.c_str()); } diff --git a/src/core/emulator_settings.h b/src/core/emulator_settings.h index 660a1a1a4..211f2e478 100644 --- a/src/core/emulator_settings.h +++ b/src/core/emulator_settings.h @@ -72,7 +72,6 @@ struct Setting { } /// Write v to the base layer. - /// Game-specific overrides are applied exclusively via Load(serial) /// Set proper value as base or game_specific void set(const T& v, bool game_specific = false) { if (game_specific) { diff --git a/src/images/big_picture/controller.png b/src/images/big_picture/controller.png new file mode 100644 index 0000000000000000000000000000000000000000..398aa176af49f929c9c67ac8f03b6dee29ce36af GIT binary patch literal 2295 zcmeHI`9IVP8~u)9Fd|DK$rdw{gt1=fEo8!Ln~c=BSq6i`%-AV1_I(-K$U1i@d&v@V z?-&Y|D=oKsf0psI${)Y8_`{Y~$p{-w)T3=EB~ z8lz0jEiA3k*KKTX-m?Gwwu2MS#SQQ7;d$q-mk)tR^7RW03JJY;Km0)iIWj6bCN?fU zAt@y_Ej@#hnUzh=dzfEP^as7P?9thQ_An%`L5Me=^#c9i1;g|I92c09|Hi7otd$rZOEsU ze6|w?FWu8lpsUmPC}<7KM1%FrixsgC?dl9frSE{Cu5*t}KS<`q(XsWab}3L%6!JRK z%X>kcYir-bM}5@)IV)&X&x1pnYWHFN3}cjx=-q#!4_y2otk0Xp-teit5U7)UPKG+C z>$7>G*5|%JmYYtW{-Plc7I87-`RgUw7EV#{T47(-I}NsIrux)+&Hj32Jcsh)_FHiu z`vXaoCc&DDW#Cl0=X{&cLrOjgF;9^f80+$%a#zQt)D)moS1VoSEjMNIdr)Va1JBaG zRNhZ>x?DQ&vd(i(TdmMY#r;98)8BVFpH12=GzA|+$2Ku>B2?g$zB%>q-&OY0ZfI~=L5=_ggl4LDKsxZx>7r@5=`Gzf+bAg1J%`c}y`-7p@PUr)e-46YB#mOywtK{T!4ouo=mn7KwN*ebArR zWJ+EPCcd1AVtaM$-42(#ccZ>&Q36ryw4OW(1|4iw0PKfxst+}D$q(9V=1D)J2(@Sc z^cUu%?an~dfZPe1M!k8N86Ex#`FOTC0gYf*^6bL$-7M@Qcx<$ogYQFIqpPs;{LXed zz+Gsy?x2TcgCf~-78tT@*rx5sIJjhSFVHIX|C*F{OhtHHiO~G-JL{H7vDtD`RsEQ@ z?u-731^ESiMbKVC(3HkfXlHMP@Wy2?G_i57EIRY&DLTXYGfQcBdtQC(nYn{)c$#8| zW71S|8(xG+&S}m1Tc&)ec9c4e*~I2yk!zuT1O5#9QmPn*I89$lMn1KtJX6jG2mD9p zi92SqDeoryoPipg86OZ%oEy)v4TnD!+tEv(k-kCPFkwuZ4S$(rn(T|@B9}cvEANG+ z`^Q#oqyDtZG&!l(aC!^9spr_)XF6z7Y{ieijR}-9s)(87{~(jJMi_ik@cW0nlvToD zc?b}>DER|&`4Kol#k7u)5t}U}SEu;WBiiN3AJAl*FrA8dsj51ao;E|?5p-;-S%T0z zD_Zx*V?Q8{jMC2qL#-6MMlrehW>$!@|R`f9YvHxS>8Ho`)x zMNd1Q^tSLOr=pdc-z)@|FmBp3e%}oMbJ8(i1$MtW$Oa$hb$gLxlq21GXmkh09+V4Q zPUQ8Iu4byL2CY~@Kq*Qrj@VXXvYN>Q1Q*V)&7Iq!h=jEWVQFV{?M-)usR?j!d#;ez zZlTl+-zyU*Dd7a}U9E(Tgti%0a&dMCee~SS3adnr*6tLE%G7_(En)~=vrj?-mSg}*f=Mdfb=Exi+{kmHqcfyGKr?Rv?9PKQ=4mo!a+8XJ*3=Q* zIQ!J)j^vy*4#dZv_iBkZKWav%Nvf=GV|gjguznzvVwCr~SgfQ0L*k=f}+PRXxP7cVmXK KGJay@9Q6-NM=&M; literal 0 HcmV?d00001 diff --git a/src/images/big_picture/experimental.png b/src/images/big_picture/experimental.png new file mode 100644 index 0000000000000000000000000000000000000000..8f789dd608934ea48a41408018bae356178cfbb4 GIT binary patch literal 4299 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 literal 0 HcmV?d00001 diff --git a/src/images/big_picture/folder.png b/src/images/big_picture/folder.png new file mode 100644 index 0000000000000000000000000000000000000000..d36d2440d1a7de6fa4a588d346edd019d67e7328 GIT binary patch literal 2838 zcmZvec{J1u8^?dMFs4kFvW{IsMOl(9Bx|lo=92RHzlIXU89`B{iCj9 zulWJjuU}UW3JbyeU?cq0!>t6?TF)%I1jJ>-AMm7{34x3|3)u;KaDpUwvGu1peK%%E>&&NvPg50cUwt3 zv7{GBO5_ue6!qWGU@&5@pb002yV-Pxq8QsvqOZezH36}c0%6r*GDnKAsDnXD9-08f1M^1~f1A5i6T1uq_(KY^hK5Kt$zZq4wCbb; za3l>pkvrr-=m%GJYd>~DZTP;|a5@E;m`y;m${atc-cm+I`xN5(PGX4VPRKEDB?`>fIeqmkj!3aKo@NJ0&q^J{F>zf0i)M7oi*x z&oDLoBccaMJmlfrCG4OS`Wd{7>N?=yw;BaHhic+FTNIoEeWoH*eRDr8tU3&rO_|vD5J2PO9_|{!jOf)x6 zCTuiU<@9BgKs5;(0@i^U_Xw*a>VCfwedXlOMB5}V*VMZpRVapK$knv5F?3@?=vHeq z6WBMsUiP#&WuGC~C=tHuUIq8q-ve!l-sD|kCO`riVyZikSiW)BY3cM{;zXjCJ&Ci- z4l!myH`g!II;1zjXo!Ow3P^nU)z1GR9qTvK}+!9*lV$6j|0Fo3d? zhyh#QLQ)g?GXu81N+z2cu$L%t zq^%?rZrh;9dF?)l1-0d!aB0c3@-dDNxWqV@`ZvLxR2D1Nk;Tt*48xgghHTmF=#msD?N_`76qSrM0qiajKQ)zKU5ZWZ=ZNYP9fxrV+HP>%#zGd;t%=ET zZu(&b!CB>CwN7QO7HV3EC8rR8;k+JJ;;7^#i91>939P)F)OwCYMH_H)HPdw$ zKNDX@&Vw*fhf%NG**Y|gBV&)d&9^&C!O*@2JsSA7VPzX{35#>gpyzfvgQo8|om9)! z+Ypf`=v9iMVQ98qD6tfCQ^jy7*Oa#F^5R=U2wdL*E1Fu1loMPX6NY+9=`G5!|H`9Q zA@$Y=bJTZ}FI$|@XlpKzS&=%e(lKLc^J>exVzAclQa3E!3(Ia~`cdwjgf($e6w7O7 zM&_&$x6nC5M9k%uK}p+BnWbsgA;dLizn=1X*)a)9%!P<9-U)%ox{vtD`)7Y9Y1dyQ zMOJLSf0-PFVsN+2t_(=ZeUW$Uwjtm&ri@BE_8e)KLWILXv#f!?7E%DTB2BR9yrgXl zi8JweX+Qu)ty?oT&&?G~u{+FZU1qivh?>#Q9`&0kCpcQKg{JM|3HRLXFQ|PeykCu6 zA(Lz8GtAbn2D&YhT`N^fduD2gBMOUUg%4*|pto#`RcM)3MhGh>?-z1Njc(@x^XyqFo z<=AKw%IfSQ1=7^hY-+T8C=g$54_`9E7KJTtFIo!W>{MlVznRR;${Vnid(S42`YKj^ zdJVjBT@YRf{yFk3e@!zjcISisOs>DzrcIBznt~^#LMpQ5Z7Re9f7(Z0hk0i!r1CLN zW1Ul%=}o^IojPEF5v`w%maab))`^HXuxVy0lVo6N*h{G;>h5A3x5u2?BBdc7etJFu z0~L73{P}{9%#9!n=ya#OD`0pSzhiB?mvXFr=pJInC?Hn5Y(^wwCg5ZaY)k+fNnzPrWB-g?3-mu-%#Sxz8 z%MO)P+!fQt5#&G#7e!l%4KbrZn)8dx>lGG?SjB4U@R>%eePKz*%96M$1M3BGjw@g} zQ4o}cJog4~i$xvL##uN!o`yfz5lEB@v=0eq9xCq$99^BW5^_6~_~(!O_4lTG0nqQ% zRpH>VMLk(OvcyiuXX4m5__HP=I?3+2v!;6ZBicn&?_-v|n+qNNo1G=;)M$in;H_p1 zM~!wE$vzU1k)9v6tmb9rmGUo>1zt#)pb|7}$mhO4J-;=hQfyUaS^Mas0mbQ_U0GeU zgArsi#$>zj*BHl!Kp)5fCBk1B`RbU|r^Zf=Msm<@b1|BY`ef*6J zG2T6O+U=ClLVsqm9`|VlHTI>F(?*-cr{k3I(wlC01ghN8TJp?xTQ%HGsp;6H-{VfIKzYO{GD)u zgM&rfygdRO?0lU>eEeNrA1E?m=-@zgjn!{&Z!z=#|Ih!|!2g>XAmeUZ#Ylb&hv^&P z;NsyE5E2msNJz=ZDJbs%si^PL(9+Q}FfuW-u(GjpaB^|;@bcXQ@e2qF35$r{7ZaC| zl#-T_m6KOcRC=HcR)MIhscSsc)Y8_`)zdeC8p4c>O-#+qA6ZyhS=&6ewX=6{baHla zb#wRd^z!!c_45ya2L=T{2?>4rEG+zaL}XNS3?eoz{zXD!QgTXaT6#uiR`$!BSGjqw z^9u@#ijgIyWvKFs%BnZjHMMW+>KhuHnp;}i+TWo&I^Tcz*wx+hskg6xU~p*o^Oup) zvGIw?sp*;7x%saP-ximaSH7>Vt#54p*xKIN{kgY)aCr3V_~iHL+4-M~%d6|ZH$MSi zrEqX0H?-7Lj2RgByGe?5H$M>_x%}|9y82*p&J)wv!ih5WFWa2t3)?gW37{Ocs_AII zF4Ww@U|EZ#`-9XlHIlE8vx3joE`@bUKSrNoey>IsbHzTg15wlhGadC|b8&GW|)@k$YKvef4C@0mQboROY z@v^kkT_F9wzW3bo$r4uhz2KQRRdmanRTV25ZKbx)gK}4u-RbPhPx(qi{Cd^N4Bm^> z)twpEExt3b&P!<5H0yM_-FeE$_3os|+ZxX=UEpG}JMo0UK4eAlTRG1fSBl?U+#~YL zYs6SC3P>*ov$$j3cmMNa#Y(X?F0g&a&_i>{@bC@EUfSBG*nLMsi|HoPwFK?t2g(hD zw9g6+nfzAG)?SJ}OCtYz7_6ycB^x68I;~IB-}V}1(~HphyVZLC*poR(dJR%{9JV3> z3tJkEZcmf<@)Y6zYEX~~q!&NEM|XeHE2N%g4N@|66|NEeH~I~ie#Z|91}Xpk!W7wB zi?F`Rwb!#OehXH=C`!jVs}`rjA?e-t5P<90{v$$ur+Cdm@Rh&RPrS)Oo^_%$y0 z*zY}BqVt2fkJ@_X*%14+!q`4L*9FbYT1R zNx227CAPj+$H?cCt+A;5Wr>{ZmN*$F)1Jt8$SQUq_Jr!ZBF5z}d7@kLF9pnKc<|1% zSoQ98l6ZXgFHYur?Rd|{`JKNTST`}yH4Sr)E{@a>*2H{#MI&*PW0A-}SM9z=kaJX1 zwCdYVc4G(o(zQZe1hSRgb5FTz(f7`{Ia8c&w&$a60^=XErj{&deVluvPZx=h7sX6X zyAuPiVs^wWmR1q94-9hnR2uf;BfGviMJsG5zg2eSp(UZUuMmc-6sh>LMt8o&<|OTY z-nCwMceucjp3)u7zlhexDL)qMjDVt|)*qpfpFfr)hz@^OgP$6rb0pBx;WAOJlvXk8 zxik{Kov)8wtLejoM`_rXEOTS;j_nh|y>6RKj^YykHaESe!ozR^{C@nYIWtWD2I^rf zZT-!UO<(fw&caJ#omB%~naNRf9>5Xy1za2uJT+QEa9uSlakVo$K`D!T5<6A9AI!7r zQ|jhoLQ7+hnZy|wG_L>ry4Lab6MSfe`CeL?7M$P5m;btyV&79o)sC-^oF?eZZeh?X z;<@qDDT5ph-ZeGDV+pim6XlJGGnp5QdlCmmbAS@B2VO#YJsgpOn|1+dC>4>4DRCo711u zr5HN9vRUV5M9k0G3N9TPRK_}(%0bumU0re-WWvxnC zNBtfvArs{;M|q*h>RxIT*SYSukpji|oxx?xR2IyMV5tWsWU9bV0&kH49d!^+FSn6k zsu+wcz+H$3;i;kKc`5wBt+ys)W%AuvH4=D8YQYQ;!&thve0(7eRC99&en8b*SvIz9 zeh|$@6U1CQVi55PBZR$^^yC5pB;@p}C`Bp$)kdz`fT@%Qu&-@=@`I6WciAgkvXK)v zH_j-r;PHm1z(kGl2E!^qxpHk_1TBVqjiRo{9M`MishzBkWcCKneO{- zV5pmRKAxD&QzHm8kY+tziZTx%F1T!zk78EQ5lsAqo$&I_J`!~RIQYW81MNPI7oMXT z&+UA`Mi6YlR{KXZLJGUYl8TOkHdnhn_YQRQT|6(n^HlDsi>jlz@Ow&Tj8~YSe5pX) z+nYjkwezK97$}$e^2{`f0z<%4aL`$FQ`@>Sy)nHZ z@ZMl{p)aM`QT_E!urCp{a0@0vu|r?^jBj`iwm??71va`-Bx<$Z1p|Da>bmJlXuiEu zdiR?L$7OD~d_y&Vjc!@C7s_r7qui%%4_%#bYODfc^o8_QnyVXL>(^9~2+o|iZfa;^ zd~0C14B{B;r=>L_OuZvfMrIQ)o>DCpBJUoS zcfo!sHBS=#1N+^p6+*Q52W*3H`r@6vuHoN9?$iX9W?!UXwp>I~S#q<#SPid18GgqU z@co;^68zMj*ew@}cLXpjaesx{X8`6xKe(-%pIUWH-A)(Byql_E6XyVibN&||Zrx^N z(w&*xT|8>l6+2yk`O7R*kqB&j*|=t}?IcimF}2+l7O$Uk1@fsO4%)qNTMf{}#7A&S zFs-m-Hlkp|fd9~yRjt%rf#0WuzExvN+vp#BGP!w@xHVx6*?D23Rum(bB_l=ED!_t5 z?4)5e&ND`8F`ogjM9~@Ah^iGuX30n}p0Sl@H*tK%F!qSmb(3#iuq|maWmcp4-Is@M zO$psLlcpB0DBUpD9z4EQBU^z$P(}P+h4d4sU43m!C?2)^q_&5q1co;Y84EWel9@Tf zg|K*$L)b#F6Ndwla7~ONMyxW_K`FwFgWF5OiM0yh>d%P!YmO@}@MEgSIt^>>PI^~6 zGcL#JW&KA1O7Uz%9-yCd0%?;4bZ00?A;#N}CrMqA!ex@Ox{s8ol!&0FXJ6AKis)=2 z@G*?Z9^ne>#^h6G1#U>HB^sr|Oo=wiF8EE;h=|`J=XZR++gr%ylgf?3W?Al(^bTa5 zlJcCEBC6`wgw0O&KEM6Bu=RCr`7ivJXveV|ugkz2ALkE$kEiY|FC$IXo5k78AL)~^ zat>VaJ1s?T`%*XwYeto^yCHe;Lf$;~{rpL_-=Ltya~)TC>15yfc8UHlIpl-SKzTM) zliuPFL&mUAaam1XJWbFD zljcnZm;F@JTA`0_He*b_1Y4j+vNE3va9QT=XEoiUn9K;0U=oh&3T*@dRNU!#yrd$|jwarIFxJKP|U1+(=tVh-qZu+k2@P!Dj zc&9MYZO``kr%V?W?W&QiMtR)yp^gODF;lnAuj+wA`^742>_4*~M{KO6fl3IKxGCMhpg!plS>d;7#K2wHJ1F zb1+~fI#s;U0MdJrAbwp7G+r~AI)1Vyd#s^DkfXxC4s^>i8g?ZxI=|RgoTv$FCyKgA z68Ep>=+Ui6TG1{|qYf7OPBJ8Of>xu+2zn*Xrtavb%P+$Ty%;z@>OK9b)JIjR?BuDZ zrj`p~RDR;ynHlT+tGCqy{D`HlmvXorH@)m_s`$dHY{ji;s$U$D*?`{xD(PK3&s~ti zgOtDnWA^o%G>LNQl=N#sCrQYLtW=S>#hP+X*pRP0I&7xLV1@PX^(Q5HCH)4Gw#Add z?+u4xH~V=7a^&1ASxrYpQfJ+Bf6zkW>Dq}>otJ3`O#H?HQ+Pd3 zFp+!TNAW(kQzAG_`qTF&(?1V;1hi!>DAP;qDS7|A{w})wZT5avSDlcNy&ODS8~RYsAh_{h!vog zNr;I8+WS(%nDiyglhO(krMp^$X)JrvLTJ>hsZ<>=@J)Qm<^z03Fz9p3rRM8Xg^PpZ z<_RTIG06G=a|av)!y2Xk(1`hi$iBaGz2=PV;Rk7xe5EP*lrTQ9*pX>Z|6SH8)T>xJ7bG8DrPV$C5+M1?{@$nmz-jiuoDt`F zkESqQNnv%LX0&)uT+s?fVYNxGy8gKRJj@^7whDJ=3;Yp z5VjQQuBNHRjSO&xedPr)X~k8Re@~v`m`=7ggG6F-8|eXZeiP_^Lr9yfHsPu+#XcR0 zpa+@WCB?W1)R&|$vDcVDBvjEp0TDP5gKycn5;PdtQ-I>Mp2cr2>SvK z)HDIwkGSVh7hS*lOq0m~c?!kuB~l+Y1RJ7^`E2T)7~rGA<64!$TVJA(k3r-mGhF zEk`Id9}!Eb69X?iiz@ScEvOO=IO{g^;TQ3E;x;jZBngF0u1+aVxq zD=?%$b4fv&-x5jLqSl^$JeB)`-?x~u?;!FY;Z88;b5fSd-xLKQfYsUjQAON`7KTEs z+E^v}i}vXw?(%1D4_*P<6n4}`M|^+qB^2QEVvX)*(D1|ehn9Q;>wEw%%dUFE zn}4EY1b}AQD3|h8iQjO3`2kzbv+&&wA>9lq{>QBeXqHvKWnM322hj94tF@SgM2XHqjvO=6ibKFi>a72`x2LH8;7vV*@0uV>e5_|ap0 zZ=OFOIQulFPA~o;N9q+NsCQgYF+{vf9RV0Z~L(K>BL{Y`)h2~er>B0AZg zwiqj*lnbneb}t$ilPo&<_XEfIN{pdju=ZELNk0YLrp$>krUzFt5C zrAj(hbZyw4e!Hn?V(Tn-e(a=actHC(DMDHm3ripc{RS>xDK8A4awL^c%tk|x=n$sh zf1oqiHb00#$I=9-p*%jSUHK;-^7ucbStC~-8X5(N!WKso<>cr?s8}3?9r6$9L&Z2V zwBcpY0{1hRtB0hq5NezNgss0P%xErg1b$5D8^jn;EQGQM%o@59&|tz_v8w>$LremH zvjoHHR9Y)qrO%sG30EEKB^dYSg^FbaEvx@&E~%|9=E{Mb|HnZr%$34eQCEQs_+V~o z_A4|E(m?*-G7dBgl(cOVqs}p7*3BlYqyk2JO#dK;2BfTrgx2QFqKJA%u)+rdP^Ef9 zV(}nGvFdFwAu3X)saY++YrEzqR)NtHbMF7h&Cm$mAskBfqE(&m)KYjFSN#goFc&A7 zr_pdrVJ4YvzaFa~`TK`|ysiWTz5BbOpt?v+sZ>8?OBgsrt=@-`UR%(|Kt;`Rw0->R zvsz**Z`yST?`>a3{1avddM+|%PEt=ND_6yz>XP%H+M$DJwu#mI>k!*8e6<&f{X6Gg zA!;Od)_-3iSxVH!*PAULkxYCVlN|}KN&xw1`GvApzHn{K@e2(E8-E>^t&QbI*ErO$r9LtMXz6$SKq;FABq8W{M%F<)Bw2~{AUg#PQ4_y<1s z^>g;M>T~S#zv=2vT{;0n>%{eQBI^xXz9lKa*9Ipex1IoDsS^5`*G%=sK6ynVH626I zKy+rSY4yBX!)Z0G7jph`CPCWBb7_}Dqg(r{A0gar=K@er?N&`rlmD>c5{fyKi72V& zH^tSTxk4Ol2C5I{KeTjFQL#>FiQvNQL7G5h+89}!h_WW{Cn|x8@!3--G}(UtKl9u% za=pVMe{!GpR6U+&%2XKyQfYXN{AamO{LMt8S&t1&J_rK2p1QdR8Ph_ z%Q6!Eev^tx(9MO;c&}od`#(-Z`G1SC$PE}!#Bxf0C@au|{t`sQ{^$0l_g{^ygx!pO zmg!1=G|hP~8Plt_@Cya*CM8Xd`^W0OZA8(^UZCG2S+mqo#ndzgh-Dzs_5q^&-xhH1 zBvhPzGhh&>^S5%qJ*GFhd4`hI8M-a_k10L%o2{;s2k*hi zuzw=uF<(?vWh1w(c8^IR_W1is1aoW{mEucbQ^&iUkKe+$v#hnOmmg|=DSjVi>ibha zA^X4Rr>O4l?o_E$A(9JtLCtLc%F&HcqaQZUA`i}k)sXxP zQ_dD=r{i1&Xh$v{3+OO2TYH8^gW$h7EH#y=!$PkyU0$Y*<1hbQ8)NDq3A|- z$$aMjqL5t+cXf6e{HBNe^}k2Yccw?HIlZV552CSYldIQy+R2+5vY{d`)5^o6S2C{s zk2kJ-se((-PGMY5zX#Ulb1eRa<>Pk$Ad-1!>@rVJCZvi!ggy_PcR#{Cz1(yb-Qx** za`XQ1{P6bXJ-xSPY<`v%8hZaemAH55ycV|L00Xl2Pb6-$l{dM}-(0;dTBz+D#TPVT z|2F)5pT!Y83_&+;z%|?B?Q>KSm~3zvxX`^W?1PWA@VR85{`@uy+H$-3OZJ0bRoW5D z(oje_foFkk!&Nc13R)Tt8}51VHo17Re^bKogI9DhjP1b}NO%p|Q{kQ2mnrq@Bgk>r zN^B9=^hS_DcR6hm#IuvD7&~o7MA{LAA1${?)k=Ln)gnh>>mY^6(pUyU3z}2#6gr;x zbxP!jO;aw#;v_p86@RCm&(l38wGklJk>GEp$JSZn0(K&OA3ov?9G#8YF+4M89H^9B zc!(^JvfVO$Gru%LMkZ=lLrR0q(K8OszYqBT3{(X$vUM!1vuoX9Db6O;syF;(IUAKcQ01wmKoCsbXA_HuF|p*X8so_*I!NZzXXTp9_-ERG0MesK zq7|FS;xi*xx_^WWcImW#NnBrN1<2Ke1VQ7}`J!GKAX%Zdm{9oupYdNoalE<%g?IMZ!6xcKRJii5Mo;Z@av%@!YhF;*p8=rrc4!oN}yOD zx*J=(!`+7Fj|6J;%JmZ!cvm0O@=McUn~LHW!Hf>7Dyrt_gix=iLKL_2DoFNK?151p z+^Oajp^jEZRqpd34Xx`P9nuT!ZtSV(Z%|XBSSh~Hd)>b&oiht$wz7Q|{{fz)H#n$@ znY#_A{n>waxeaN!`PUc;?{0|2l1HFg$kTZpKrcz=u$HqYa?mmjG!MyFw^P72NZ`2E zBw?TKVK*Uaw}0IjMTV(Z>z~)%!!kmoGF`%0ZEp9OH_hXdEj^;KiNUsTmDSBfVigiX z#)A%2<~VKGI!z{lGk9YlMB-j5^Vu|6o!+(YYz8a&J1<^4JL*Jm5VaMy2zkWKuDX5X z@~Pu-Xa?~Bf55>lMSkZCq$GeY&!~SPdV1Hh9h;~c<;b)1zor%z0gzMZ4^7d?45Pbr zFQGk=XwfaB(&c~ssmYv}WaEmL8A~gZVpA?sjDpS^nPgWfk<7Gz<*r!~Y9s56O?mdr zjHD;%tVota-n)$qq&X0wkbArvg``&PuR>Wbeq@Wd@O~n}h)AOZ`I|ysM{-Yl`CZE8 z-&Z&dky*c_aSR5Mr$r9qZcp9K6sY{MavE_bqFJA{Qs--L|K@J#V78w*26aED3(q39+@~lKQ4lOul$z^;cNxn#Dt0uT zy<9QO^0mZgRcyfft}WAkT-a?ycgW$=0|`)WIZmwuv?&JfA#FreYUEJO-H-KOzZ!#n zwoyJ@LcaGwSDFf>U=AU>8Zrcmmai9xHJ<(X6n<09+N-z%tELq$F6og~4iu2Zx7PtV zyrX|Ep}}U?&SL%%9`qw7-Qe^fRQ0v<>6^9+bNtdF)Lh`t^XvI@7vXR4R|u~oNR33` z@NNM>eXFim|1o%vPJ5{9qkv*-;zY%rUTyDEjn4*g=#BL09VP&;4>HMm^H$v@2Z9b;3^h^id8T@*qo`azML%RY4=07FeCD>P5K(uukfmt`LpMkTHz?$eR*8 zRFT9L%^{tn3L-%O-l$XM2i`;DMkZ(f9M`NzdU#7nE`mHxJ7ZjJhr~U1Lj$MxO;5E z?;2EBl=o*0F}|ObVMMGV$sUl#nJFQq#Q4^@V?Da`ddvc_gvrr5kukkYTd0-5fNC^~ zv($l2Dwi198<-_drb7GiLPuHE&M4I%3Jn>gx>?tWe==v7`NGD$I%vy-J*;hH;GEFf7azX!TKl2!WwP3`05*zgkjL!ZE!H(5pA z%T~fhIKv5-*nGAYX+HFAiw2rmXAsHPlJhH=5eO=LC_HN%Gv4Ciy0zWp#POT!ben+l za4c(z?4>}^4M*bc@#Ryb-f%UKrOJ0a_XY_HE@67Z{=%zF&hH^{7M(;5lv$bI2&5CN z<(w5}YLtjAmVDEsIFEpVN7h7<5*0?X{jZe(Z+pk4?c{{Ml;qFw+J2t$9M?vlTdTVU zEo3+XIyG4f)qL-LJSfH2vE*)-NP$Mz){hrQ`1h-5*AjmeDHd$fR_RcS@6@IEaLiR% zeP;@3c9+VSNNS!;unszTnr4hVVUd>+CR~+F*$pyQhyHyA_cRS5I3=2K>efbtWi{Q> z49X7~REXaNnnqV0kP>7b(=C~sv3Hkwmo@6u!$o;VbRufr!CgbNV$6uL_0;d1&`lOZ zn{m9VuG+h6u6_L?!zQMN4eq?f4`>Rg68PyzFySb%bbP9DpWSc0@6=*R1iGh??K zGHQnAy0k0E`v!a_`_+h^17t!cx;`8IvhjvkZpsx&n=s=Md^rAvHtwQB1w^e!TFJqR zP|^vk9xhI^|5jxS^fZ5B-}Yn?||%#eN_izig+#b3nH z&7+CnLf+|5b3lZmeomukY#G_#td{qDJXY#t#M)fc#)S`q*;F`XU^xa*&Ag(wds3WU z1gP3N(x)#h-H^mwkRAs@c=vNsJl@(;>73%UukiKHOldjjl*yMli1+v0>{^H$JF}Uk zn%rzrEezck!|xXvdM-)Z)Zxz?PGE<}cIeoFVMcPse}6Y9vq za((6us%=(ziE3OH9qWf7K2L2cgz^8F8tl#O$9{e--PyPmpDOstG+S3b;}PM(^vx@M zUG?JKhm|vb0dS>)`Egut$z7I9ZvkRliqh&7p5phS}t;r57diQrafL0 z;|g5z?;m~Q_uUGYKguJPzC2<7W+RKQk76xxh@1QlvmXn~3gpA9Eq5LPy2|lh+%t~z zFxu&gAuzAb7*M9s^?r>rp!j$aA*$qC-BV4N)%ZCYr^2XB1t6K_PbYYGhkQ`>p}ssw!9AtkE5! z5=YTIhadR?859UChU0oUDI4Lv!CX8(F6)^*Po;@gh~AzggBli|<#B>dlv{pAgFRfU zi6x)B*8d_%A^5`g=RI^Yk-%v2`$oWc_N6emIMwy1n=90xw%|{zA_OU5trHz3=;lF; zr&kgFK>%<+rgb@%(uD`3Wh{09VF3mXRwg0`0-P8 zkT8waW@*6$0Gf+D0^(VzSt=k0$lyF%&aQL0Zv$f!$d$c*Ln8Xg5q1D*_L(-AY|L2x zgaH1z*n<%|Ib-0#N>$9Nx@t{u^vA85^0(&Sm!rD6kv5EQVSM+{aSH{4?P2JvL@=>i zHNRvP`o-v%YCx^{wgnN*X*ud8;D|ugP8iP%S=&0k`6U|eY#p|pr7|a8ERboM$I-JG$+t7GWW8nk1US32W7Sj%Ua43bK)8MQ3grEnCI4ZX?YWQ^ zB;Y*0&m+LqrMQ4*mq&%`4e(_}&|!sPYA%Oju)3gwryO1W3us!4YzxhS@VEG^Sf1d6 zWtwCJPhKMHNg|~uXHCYd_s-!03Ka5=<1OUdz7`^cx&-ZU$a+BJ*~Bd2cuh|#+?Atw z5X12JnjEQBD$YJ5DLV)Bf!9o&w~6Ugx^UHa3A^=AVqtAB7vEsct`KrW4pp(xzDY!o z)a=v?{rXKM)DFLmx%(L*?XwB;m!0>dq^Ru#qq`|oBQhCC@qyp17UbiB_oa=N@#bA0 zxa)}wb+)K_sFn@E1@e{&xcZBw-tnBsbWY;TDz1oE8UQ&a27v7pxHqY-FF9%4eM*IH z2ed=wixxJN3t}8liurxuT&;R2cVf3BBmzBsC`WJ^*^1^?)3;G) z471zhCTe)ceG||Rb8p$xy{`B~L?lLSN_78~CD4_k#Ull0=xg)oXA?krf{v??wYbo4 zN8gjtp2v!J9F3atmu;?5-Dc!(?+Vd;8|3Kc96y=%iX+Cgfc3ka0+~=BN@_i9SAVmR z{^>XEKb&-yAox+3uwwEA6@m9i%5K$ysDUkVw8Gl~xJ{xx#BNB*nJ%6!{UIVdmqy_T zOf60+&O;fjviAN8bcS2=#U5C}5fOY($|DXO!0%>oP7nj>k+m3B#;04DoZQ_Z&o#ij z62V(PULS#2N&v4{nwg3bXpW$oX>&Mu3M;a6Y&qSexMvdx%%5Z<-lu@K{#4Cq5}@<- zpF!^;$K&jue*Odz)u?ByhuX1|eJzXtA3`fsvw_>Z@);+dS}|QCUuoVk(+M275PUPD zqzi4V7X(&1i%5kML}mm{A-&o{i&x}6inX)fwY8(+UeGkxv>Jl&m8MZmyI2a~S?g`l z$sodK=)@~^?2vZ2+!omv%?h#-ES`|)n|;;*R=B+M_; z5Q{G$Y~>=#_c)+mSa>k}_-BXZ+?2ZJfA^{nyRbu=e z$P*N3ZpnsN)I6tqQv7&lN72;9yfXZi;dfhF^Y&y!elmpm0TLa33XOLmq+NUgJo-Jl z`H}9atsP!z&lB?_A-m!`Q1e_9+l>jHrsLiL37q7c}Lc%o&SH9MdFOK2d zNI=c^!;GhyRfNhjDuC3yHv=C#@yQAiNz^BHo7O~mksKm|#Thcd-c#eIiRkh6UpTkp zK)Rb$!*xD$*DsJss^0g;zw}5+1uvLsTh!ti$TY+8$aupYrswlX$sUW3mm3pINTt3F zGbdFcN4}{53XtH3c@mNth$SeJka;~C$v$T=SwDvo^MvbA`{1qlWe6FUrPb~g+^=2M(hkRP5rZ~M*qYdI=e zZd$aCGA35+RRd5Vu!~w**pqpQ$v$TTb2JO3DK~0bH0UTwTF+nnJ7wiA%?gb{G%TMq*;}+&@L)P%~|GG+}7|nbPUk$5iSL0#bi2BxPYw zX{F}z(+!&?-^xn1A8<&a+0`A%XGTC2al>EA&`v-`eGg@6#2f$8s;M_TH0fKFdrbtg zFOL|r^U?ez zGc1*4X1jY8d5a0auV4}~(MSa;o8bzZkU%K>pq%=#W&1QbLc1KyGh@KNVuib<&sw0f zWTQqU(3&g|`l+SRINh3zYYRGizOUq4w*2~H+S`ng21Q(HD?p=L5`_!g)eZUhz=XL` zdr5j=47i=Zm7S(F=kEdO9{LLRvcQY6Ddt{TG@(4}ti8u(rzvz&;!68kuT29ybOQGX z5{n-xR(*Fu)G9zkb{g8*mWZ6BRu^`wFM7*T1K@Q!pQOD_bByQRN0NPQD(XZU`Vkx# zF0{^dC@?@Wsc=OsRb`046VY&o5vTgel(!c$;m4CkVd&cxsT>EKIawWQdl zZOw2wkYKLrLuaX&0{tYu0-)AHT?Kwqaks8~{RgGJ5f_?A5rY|%=RnkH4ABy3AG$&_ z?=b>eQzlL?-+jW)%Q#+~SLFX}y&BSwr;&ckiPwV4XD{yK8g?V&&yh%Ad6NK`j`A;z zq5I2ULFo8|PAuWK3J>P}mpqlk{;U8c$Hfs)qcmfB<-JYgSUMDc#XX+s2R$HvDXvKl z?V%HJs*pA#!BY}4M)d~|9rM8auaQHdS>m{7Y|b=8%J}I!0osoU^nO5xx9S7cACwO2 z#6y)~!^Go`)TnEK!br;fggxO@KuZqHCW-iQ)zi*uo z-;xaL2WO^dHBv|qUw1i-c*iJb6$~Cd3S+y0{^(O&cojQ%s)W*w<2y7OKCd_O;=XP9 zC4aq+RwBQ1tJC+ZZOUbQvT!%vYKq^9U-BXrR{qJfK=oQ@Hp2do!ORYM+=|6IgO}M9 zV#%tFCug-Zq+IZV;diCPv4syD4@^x3WF&pd}(I;2Q3wr{4Z=-Mbpl?sI-$ z;Sf&Rq!=A6wT9wh!#7XvzD+Jo*n8u94|`Wb*9n%o(~Y{S`k~&QiRBhwfTf%X6PYH< zBivJrqXhqPQA}^bT1iH)zhYn#_FWshAUOlX>7#Ls1MPEUW4bRdmK0H^e&ElapO6x> zMoX6eGxSW2g0dx=+l;-0NaBAG&B3hC?sP}qgzp|vSiOry>#CBQYb)`UfnmS2rXBdL z-ld_F9+ETY=)G)-%9<6d9Qx^t;tOPqXBf|2_~MCUsrR5ldg;5i*sedlJs43s-GAPX zc{fe>x6$kTH@;i+?*0$#yFI2sPX}{>?y`zGOv#Zt(NzQbqV_7a&)i$Wx7A5NJgaMV z%vB~fUhQzsQ-;*}`$_2iT;H(FVYcfLOVrTm-7{F5ov)ysaEt4Ud1w&EZ+%Wm?%0Go zEvyM&kRz6yRu?FySGju=Iv5ZKQBi%yIgjZZxa zP0;ja++lE9jw``6ZUWIZlsQwDYM)g>0|TUKJFUqZoPedn642l?cXVKxjJ@8_eiE0Z z4mi5k+j2+aW_veYi7|<*4V=0rqc0D3d3N%&D8YQ=QBP&VAih_|Xs6;jGTi_2OjEf| z(SGJ4iSDL6o-9%;iQ0!tghu~CPi2}3s~_ht+0q`IUWCT4kt96kL{1Skd2v#!#P+n@ zv3MC~KV(2_D%IO!&NYauBKIPw7Xh15O>(^wj~nM9ycf|eNOSeWs(8qN{6$e7&Q`c;13eWz zC2gyb?dK7;YZ0gT)P;-KvhTr#uiK#C+r;C^5@*AS0^qg+H?#iSQs3sOpX;zb$?E=N z!6~B4<^;@~Ix2##SI)G<{unGc68!?K^_1Vi7k-a4CNn0fo#3nrLH^3K9L%)^OvJL4nunthidx*6ctP<+RFH?G|r8Y*G^ z)r!ea3@K&pmfX%DmLQS@v8n;ROHc6KOd&#D3{bOI=^>EyOnJ{{XUyxCsCN literal 0 HcmV?d00001 diff --git a/src/images/big_picture/graphics.png b/src/images/big_picture/graphics.png new file mode 100644 index 0000000000000000000000000000000000000000..66fce3d4f7ae9113b13f3bee85563dbad2831573 GIT binary patch literal 993 zcmeAS@N?(olHy`uVBq!ia0vp^4Is?H1|$#LC7uRSEX7WqAsj$Z!;#Vf4nJ zFdqY9#^zi0j6gxj64!{5;QX|b^2DN4hVt@qz0ADq;^f4FRK5J7^x5xhq=2g9JY5_^ zD(1YsX`daODAD%uwdJ;@9!F2Q#<;Mvb7!qu99bGJBl?HUuTQT@{@}%)MZ5d1Zgp$5 z68U&uNzZRrDM@Ftf-@{L6X6MUQUit~CmJEe_4s)0V z%njeP3I;N&H#VPLn`|1vdN1|aF9Y%8W&VtUhP*M1&lZQwp2G+|z?q4$B-#`L`}A1rU!ec-%d#I4}>f8NZ0d*uMbi&}*_^3ATc zXV2LL{I&bu>(98o!ED?8tuN=AUidE{A-KFD{6NXZtvOS!HEf=_GsN`>(3V zIy;yh-y7yH{NNOJ>}OXT!?MhEi@vM&XJ%P4d< z-1LxNKEcrV{o6Wq>8bye|88LP`Sm;j;=|_*M_`^EDooZ7q827kzRqtv`s~_9sWVLO z?R&~p8`2LPefDjm;kP|ZyOwU>khF`>A$mKLm{)${QJs*PKgC{5xp%~E-Q=11%Ep}? zhYv_Uh;M1& literal 0 HcmV?d00001 diff --git a/src/images/big_picture/log.png b/src/images/big_picture/log.png new file mode 100644 index 0000000000000000000000000000000000000000..dc215c328f614790dd5175c07b59fd6bf94230da GIT binary patch literal 2173 zcmeHH`#;kQ7~a0yoH35uVw~DUtfr=NOP4JgA~ei#D{HwfVQx*iZH{x6+aZofw4zfT zCzs`t%dnD5r4XeMt1xA;te7m$I)B3X`MjUc`#zuNeV_M-_xa(?AiFuLspzYKKp-`z z!z2#{vc4xAqNtyR7giJiiM4a0z~S(*H%EmEs?0d-8>{F}d{6K^30tjb>c!dn#Cb-A z#4%|x!5}7+iKj<~#|F|E!T6|{(984t^+6!u5ZQ%dzrMb%(ElI*JMfFaU)q{Qy^1L8z+zsIH-^ndp9<;S1**iEMc5*)A;zoA&@T7Qo`}q3#Q%}%N1_p(k3O^mmh>nSki$B9mNK8s* zvD4Cjg z_oRPdaA^4Xi;-8OW8)K(!Z&Z_LP@xma zmXa{~@$wv%Vvk}?7W$viZ3*)=L^&f={BVWGN!!z}TXgdCwCL`BzB+!iU>y^kjM1*! zvnmscpSVd=1*#mu(=K>^3P?O(CF7BQr54;VIk=Ys4QTAaawt#&Z!5#y zUg%vSPkyITFJtNTd1#y;8V?a<<-a!>a$XiRS+a+fWK>xGTnyqd`q)Ivxk2$^AHcJJ z?~%&Z-|~k_|Hjm8LmV;~oO*zH`hH`ee^wR3g>a<}_nJGkDhjI$W@k*l%E}zd*ED<` z;dPZkUs?zbT(qsM87OBNq1)!ITvAY(tbN?PoXv2CJR@ED!C||b`=^+7U3kk5BAy_TSS z8N)IA1wYDFP@pbRtJ1GE7It!&zLT|Eggu;``o*+OU@GBsd4rJ_`&mO&+f7Vs=9GL> zOc%i~*#mj&+rD0iAmJAsOv=qKTm!ow*fK8#W%ZW@wY@&mdqW>C0ckcM-VS z#k-JJ+y&UOYLj#>Lej;)b*I@Cji{19a?*?C27uez&z>nD@UixG&~B*)wm zAevL&FBV4KdlRlqO6eAbE7$u8!B8Gcc<=P{QlsZ^eKcY`urgh`q zaf>f*j+e#_KwMp!-)?oHlPA}Npkym66&Puyw3Oaxx8eg#d?;4V8?pbR$ z#lMaeH73AzOo+RP9*{7g!KgXH`dNK4Ui?sdp}z0Un1zPMnSSp$Z>c zZVz|-E8BmTY9d|wVIJ17^F8eT{EFLvdPOvA^1!5SBgkP2wg{YM^A83XnC0pABEpm( zM5tJ7-Qs^>-rCVtFvIS7jH4DkG2%jSfR%b%qG rZESRpoRHA*w3nSECtAru3Jh0>&-6wlr~v%2?@!;!-i=gkcOvZ{7y#}q literal 0 HcmV?d00001 diff --git a/src/images/big_picture/settings.png b/src/images/big_picture/settings.png new file mode 100644 index 0000000000000000000000000000000000000000..b2dbed3ee66db083cc624864b9d12c722f5b1109 GIT binary patch literal 2838 zcmV+x3+eQUP)~ky?nh?QNgn(x ztH4bu>3P_Yjy2#p;7cj!nHiLU^}s327}d`)%yzeB;CCtLSw)7F`37~(1cre9m^Y}O zr-17X=vpD(xF3b%z`eA{67r{npGBK6>}ujK$L0aUmUNu~Mv%>|Tw?|H9AK7D6KzQ7 zM3!%3mi638i|iddf?dPTAWpX%SO+XXy4#Y+3eW?rM3!r36!aUXh2{d?$c~w9Bl|gq z%<2z&x+GcNncxG7ESf=VLD%3e@jNo$Xelhj{#?LS?n!9N*wCDCyaT+eDBmUQ)e@}f z0GEK*73I5xx3mOnFFB4X%XbB5wFGNBKoLnJnAQ@k?EnP??OKAh9pD;e`K~~138?J= zgUa$kQ=@7BfofI>B7`F1A~zgBLbL_cs3k~OPs#5`~u89vtl*CG2SZAh=&11v^@ zSV#E()z!;1d1*h==m*Fc^minHvL2>@6G%|US>!~k+f#5JIi1KjfN9_ya1nU{+qv?5 z;oHDdQ3c(DzULP-!-LfCt+}e+he+451NeA@pBKm$u0iH-Zw7zRXlz)I%-jp)2S1|D z^R71sXaoL13x+AT0KN(Q$^SvU9$+oS!_O;z82ljE1C)?DJhp1>6tIhF;Pd$A0>K_& z8O6iTGfKeS!4Lenn*w>qZTPnJ{X;+xFiO5Ra4rESkH6yKp_)6g+v>tiRHjQ9cXJ(zU*&Q z8YR$db1(Kbu=eL53d5Z10N{Kp8~!E{7p%I5`2KSB<@G*(`9X$a|Ve%ask(ojbU^gDW<&) z$v>F06WoE+=O0NZ#1>aKea})Rg!xF_45Y8bUo&{V4{-Yo_at|5R0jAI^@qQ5r>dXSB5PfZ9 zH0l@PY^SPKwnnk2az+r@>Ebwm)R1fYH zd)#TdafabSIowI$tEL*{T7l;zSc03G!xO+AL|pML#{D|KM{wuPp)RDfp0-Vz@ma1=|EY|GTkpV4 zPp-!&(LWz3BR12vMHp6r-N;_S0=y;lB48U*N>p3HID8PjB#{#EKnNXOs^TXH^E`jKQSD@&6K$~ zyqsXW#{p$baSXT%^8pzZ@ApnP&_)xiBEhTRHB4I}(>#RCc9RYZZAiM8OX+bYxX<6n zd1QMs(~QTFmBJw5dxbKxI@*l>320i}2y;utzKEnZ=Naz;uLDPsq2hwacH{{4lSs9X zK_v3YwHgGbFb#YbvBPELaTq9ApZmRMKF7nKB0^4CflTA_k-Faj-Xn0GNP%2p3i$2p zKZ0Z2qjq&EpnyN%%kmC7z@O1e{VHGwb6)bG16)E5h6>~p2Y|o$Kd86Nc?_M*)&l9{ zVFIar^==OR5Qzr*iVcklSi?!=R+vy3);(mebZo)qypuX;ba(Lk$j;c320t&5Exe7q z{k14x>LOe&z$8*AdBXdVz4UJYE4{zxbwAxEjskx~Lb%>W%H>QWXBhoREa$hpKZ6HeF92U7W?hF0a`*fU za(=v%@F~a@;AY?z^xvASazC(w@JHH=XMS^^Ty|*fEA{&<~X!A5?Cf}XtU8U0yLljQXjAO_Un`^9~SS{3J6}TmVT{DHv z=o{QMw$vF+2l0)%Giv5MIC#Q$lat5el_}4&Yi_PU@P&$$B&Wz zI(}0Oy=utPayi8t9~zy=@-0e*&igFr)s1BCgwbaLiS}*T{eDvz1RkP!wsIAF*V^uG zqQ@k1JI5X9+#j54l#%SBtw=3kzu{lQUiTWjV_^b#8rjXu)cJGP;ldUuRs_B2+V~oB#j-07*qoM6N<$f}rVM`Tzg` literal 0 HcmV?d00001 diff --git a/src/images/big_picture/trophy.png b/src/images/big_picture/trophy.png new file mode 100644 index 0000000000000000000000000000000000000000..2fbce890106ca798665e785522480d347f58d94c GIT binary patch literal 2643 zcmeHI`#TegAKnbRFxM$oE(^;JbLO6!9AmQ6XkUg9xnJhCvRn%@qmWA>GhwVHBnicw z+)`O7m1M(Vj!qP%J1LHS|9<~~@AI6`^Sqz;`Ml5j!~49ye9lnZ4!~hZ7ytl(lU$tK zw>a}BAfT=NX|`l-i$Dnk5)}f0OiB;UZk;yy#FnVVARcCT#D_DI zf{up)l9G~4qvE0yLW1aFrt!xkxNq@D06^w8g-j)GZfN zck=)MWk9lW@?Z!Qrl1H{QdUt_Q%7iQ+pejF+_6(zM^_KEOW(i{z1s-$iwX8uoSC_W zrIoeK9^1Y9@OJy{e{&!>IuV^+NC(NTZiguUardBldU+rA@%206A8<4oT3l*G7ZdEm}UU0eL zN@G*=)s}0m*V}HicXZyo)zvNR`J=b5f8fs0-Fx?kMGr?FJsy4XZ0z~?gjg~){pZZA zbndVDmw&%nSbY6vY5DEDmG>V$e)|07>+0IK_3uA6h{YHn03feMa&n+*YAyG#g&}EQ%ozl1JS=*is$b{%9;sUFC`TQIX{h?K z^e#}j+hP>ilv>LeLgA^+{I@yriM?y)`-a1NIW@^Tvjr9iv7AG)V5W?HszhR#(A#{r zxJ3Kih%R(F?qt-d*wXoo#M^YMs)8xPaCw`arKoX&INYh$@W|(>o3PhjHa1*`AMB}V z-8Cr|e@Gvlo9bAKISMowOs_p}M;x6M=`PM+eo#zL#9q9p$u2l+ONuxF3iNqMrGfPv zr66(`COf*G9ECMkUsd-I%4xTyhr0VMACvS1YdWVqgPuH0@a{DQCjx;)9jsUCY1xn`aH z%sHkuF&cKvcl>CSIJ1JTz-BoHLQQG|p6o8c-6|wR>txPQB7bGh`_n$l8w|xFssSt8 zVO*3k&&#klwr&NkQb1|QYR>cZbo3(eFsJj4szfWpDOMhV+Xe69R} z;s}HYS2UoD8lEAId{JV}hru%X(h!1Oycg=b8>WZ5{wTq*2Vf@;NaYQ(k&DdD&%}Yx zh(sxX&2#Z}RPHI_%fP^t11M$E`9&tlMO078O@(#2h~h3KVnM~F2YEWVM%gGadD)tv zCZI#$h~s)GRT9@+gIofr(EWZ6KeLhtB%gp~BBv*wy1>@$E5bqKVB8UA;;B$r^uAu#lU6Fny2&%{|9VVO~9H8LuD!MX*^|5|Im=J>1QtEQBbEjO84HtTImsH(X zg6@lpDbP$}ST6<#2 z$vfNjr*TyLHM5uI^p|)U%50N2+UVGKa@+?+7EuYRp)W257g?TVHm~}jt&Tv3T(M-@ zDLX}yT-u#XKAl)f8ZlhppKBAGc=-dRcxC@Xojv#_2;t9yYZA;EB)v5=v82*7KYU}O zENtfbJ=%+&8-b7J;OmmeuLf1J)tBrguTboD{ZIX8-1*e+7It5YjAw)Iqf?hyOuF|s zkjJ-FJK?q;vkHRtke%IQ5#wfiB_6PXX_09R5g0K#R@ewUVCX2fJBoK))y`BueDO?;4R-1K3WF6ru9E0UjMQ6iQ#I> z5tWUGh}~dm{cpzmqTqe@whWb=jJ$(ywv}Tck18t;KDpfNxyq@*1 zuRI3sU4`DZtttEsM~#Jk4F{?{XzyS;HsCBmMF6kHeNP=Xb00ShHHv6^hvDf+oj^LBAUl z?35MiDo#je;CkB6`G1qu>Ao;c0Y09$YTZ$I6j1bO?cB7xdX`Q3b*|Td`k@<>0omYy zP~}3H2raR~tKI wE?vyKHMpSEyMSvPyszYa&p*@R(Sl$T^jeIpe&I42`16&Mh;B|TgutwS0NZVK2><{9 literal 0 HcmV?d00001 diff --git a/src/imgui/big_picture.cpp b/src/imgui/big_picture.cpp index 8cf83db02..eb7ff0b5d 100644 --- a/src/imgui/big_picture.cpp +++ b/src/imgui/big_picture.cpp @@ -1,35 +1,38 @@ // SPDX-FileCopyrightText: Copyright 2025 shadPS4 Emulator Project // SPDX-License-Identifier: GPL-2.0-or-later -#include +#include +#include #include +#include #include "big_picture.h" #include "common/logging/log.h" #include "core/devtools/layer.h" #include "core/file_format/psf.h" #include "emulator.h" +#include "imgui/imgui_std.h" #include "imgui/renderer/imgui_impl_sdl3_bpm.h" #include "imgui/renderer/imgui_impl_sdlrenderer3.h" +#include "settings_dialog_imgui.h" #include "imgui_fonts/notosansjp_regular.ttf.g.cpp" #include "imgui_fonts/proggyvector_regular.ttf.g.cpp" +CMRC_DECLARE(res); + namespace BigPictureMode { const float gameImageSize = 200.f; static bool done = false; -static bool runGame = false; +static bool showSettings = false; + static std::filesystem::path runEbootPath = ""; static std::vector gameVec = {}; -static std::vector focusState = {}; static float uiScale = 1.0f; -static int scaleSelected = 1; - -static SDL_Window* window = nullptr; -static SDL_Renderer* renderer = nullptr; +static SDL_Renderer* renderer; void Launch() { if (!SDL_Init(SDL_INIT_VIDEO)) { @@ -44,8 +47,9 @@ void Launch() { return; } - window = SDL_CreateWindow("shadPS4 Big Picture Mode", EmulatorSettings.GetWindowWidth(), - EmulatorSettings.GetWindowHeight(), SDL_WINDOW_RESIZABLE); + SDL_Window* window = + SDL_CreateWindow("shadPS4 Big Picture Mode", EmulatorSettings.GetWindowWidth(), + EmulatorSettings.GetWindowHeight(), SDL_WINDOW_RESIZABLE); renderer = SDL_CreateRenderer(window, nullptr); if (EmulatorSettings.IsFullScreen()) { @@ -109,18 +113,21 @@ void Launch() { colors[ImGuiCol_HeaderHovered] = ImVec4(0.25f, 0.50f, 0.85f, 1.00f); // lighter blue style.WindowRounding = 0.0f; - style.FrameRounding = 5.0f; + style.FrameRounding = 5.0f * uiScale; style.ItemSpacing = ImVec2(10.0f * uiScale, 10.0f * uiScale); style.FramePadding = ImVec2(10.0f * uiScale, 10.0f * uiScale); + style.FrameBorderSize = 2.5f * uiScale; style.WindowBorderSize = 0.0f; style.WindowPadding = ImVec2(20.0f * uiScale, 20.0f * uiScale); + style.GrabMinSize = 20.0f * uiScale; + style.Colors[ImGuiCol_SliderGrabActive] = ImVec4(0.26f, 0.59f, 0.98f, 0.80f); + style.Colors[ImGuiCol_SliderGrab] = ImVec4(0.26f, 0.59f, 0.98f, 0.80f); ImGui_ImplSDL3_InitForSDLRenderer(window, renderer); ImGui_ImplSDLRenderer3_Init(renderer); - GetGameInfo(); + GetGameInfo(gameVec, false); uiScale = static_cast(EmulatorSettings.GetBigPictureScale() / 1000.f); - float tempScale = uiScale; while (!done) { SDL_Event event; @@ -134,14 +141,15 @@ void Launch() { ImGui_ImplSDLRenderer3_NewFrame(); ImGui_ImplSDL3_NewFrame(); ImGui::NewFrame(); + ImGui::PushFont(myFont); ImGuiViewport* viewport = ImGui::GetMainViewport(); ImGui::SetNextWindowPos(viewport->WorkPos); ImGui::SetNextWindowSize(viewport->WorkSize); ImGuiWindowFlags window_flags = ImGuiWindowFlags_NoDecoration; - ImGui::PushFont(myFont); ImGui::Begin("Game Window", &done, window_flags); + ImGui::DrawPrettyBackground(); ImGui::SetWindowFontScale(uiScale); ImGuiWindowFlags child_flags = ImGuiWindowFlags_NoTitleBar | ImGuiWindowFlags_NoResize | @@ -160,31 +168,35 @@ void Launch() { ImGui::SetKeyboardFocusHere(); } - SetGameIcons(); + SetGameIcons(gameVec); ImGui::EndChild(); ImGui::Separator(); ImGui::SetNextItemWidth(300.0f * uiScale); - if (ImGui::SliderFloat("UI Scale", &tempScale, 0.25f, 3.0f)) { - // Dynamically changes UI scale + + static float sliderScale = 1.0f; + if (ImGui::IsWindowAppearing()) { + sliderScale = uiScale; } + ImGui::SliderFloat("UI Scale", &sliderScale, 0.25f, 3.0f); // Only update when user is not interacting with slider if (ImGui::IsItemDeactivatedAfterEdit()) { - uiScale = tempScale; - tempScale = uiScale; + uiScale = sliderScale; } + ImGui::SameLine(); // Align buttons right - float buttonsWidth = - ImGui::CalcTextSize("Settings (Under Construction)").x + ImGui::CalcTextSize("Exit").x + - ImGui::GetStyle().FramePadding.x * 4.0f + ImGui::GetStyle().ItemSpacing.x; + float buttonsWidth = ImGui::CalcTextSize("Settings").x + ImGui::CalcTextSize("Exit").x + + ImGui::GetStyle().FramePadding.x * 4.0f + + ImGui::GetStyle().ItemSpacing.x; ImGui::SetCursorPosX(ImGui::GetWindowContentRegionMax().x - buttonsWidth); - if (ImGui::Button("Settings (Under Construction)")) { - // Todo + if (ImGui::Button("Settings")) { + showSettings = true; } + ImGui::SameLine(); if (ImGui::Button("Exit")) { @@ -214,6 +226,18 @@ void Launch() { ImGui::EndPopup(); } + if (showSettings) { + EmulatorSettings.SetBigPictureScale(static_cast(uiScale * 1000)); + EmulatorSettings.Save(); + DrawSettings(&showSettings); + + // update when settings dialog closed + if (!showSettings) { + uiScale = static_cast(EmulatorSettings.GetBigPictureScale() / 1000.f); + sliderScale = uiScale; + } + } + ImGui::PopFont(); ImGui::End(); ImGui::Render(); @@ -233,44 +257,56 @@ void Launch() { EmulatorSettings.SetBigPictureScale(static_cast(uiScale * 1000)); EmulatorSettings.Save(); - if (runGame) { + if (runEbootPath != "") { auto* emulator = Common::Singleton::Instance(); emulator->Run(runEbootPath); } } -void SetGameIcons() { +void SetGameIcons(std::vector& games) { ImGuiStyle& style = ImGui::GetStyle(); const float maxAvailableWidth = ImGui::GetContentRegionAvail().x; const float itemSpacing = style.ItemSpacing.x; // already scaled const float padding = 10.0f * uiScale; float rowContentWidth = gameImageSize * uiScale + itemSpacing; - // Use same line if content fits horizontally, move to next line if not - for (int i = 0; i < gameVec.size(); i++) { + for (int i = 0; i < games.size(); i++) { ImGui::BeginGroup(); - std::string ButtonName = "Button" + std::to_string(i); const char* ButtonNameChar = ButtonName.c_str(); - if (ImGui::ImageButton(ButtonNameChar, (ImTextureID)gameVec[i].iconTexture, + bool isNextItemFocused = (ImGui::GetID(ButtonNameChar) == ImGui::GetFocusID()); + bool popColor = false; + if (isNextItemFocused) { + ImGui::PushStyleColor(ImGuiCol_Button, + ImGui::GetStyle().Colors[ImGuiCol_ButtonHovered]); + popColor = true; + } + + if (ImGui::ImageButton(ButtonNameChar, (ImTextureID)games[i].iconTexture, ImVec2(gameImageSize * uiScale, gameImageSize * uiScale))) { - runGame = true; done = true; - runEbootPath = gameVec[i].ebootPath; + runEbootPath = games[i].ebootPath; + } + + if (popColor) { + ImGui::PopStyleColor(); } // Scroll to item only when newly-focused - if (ImGui::IsItemFocused() && !focusState[i]) { + if (ImGui::IsItemFocused() && !games[i].focusState) { ImGui::SetScrollHereY(0.5f); } - focusState[i] = ImGui::IsItemFocused(); + + if (ImGui::IsWindowFocused()) + games[i].focusState = ImGui::IsItemFocused(); ImGui::PushTextWrapPos(ImGui::GetCursorPos().x + gameImageSize * uiScale); - ImGui::TextWrapped("%s", gameVec[i].title.c_str()); + ImGui::TextWrapped("%s", games[i].title.c_str()); ImGui::PopTextWrapPos(); ImGui::EndGroup(); + // Use same line if content fits horizontally, move to next line if not rowContentWidth += (gameImageSize * uiScale + itemSpacing * 2 + padding); if (rowContentWidth < maxAvailableWidth) { ImGui::SameLine(0.0f, padding); @@ -300,8 +336,16 @@ std::filesystem::path UpdateChecker(const std::string sceItem, std::filesystem:: return outputPath; } -void GetGameInfo() { - gameVec.clear(); +void GetGameInfo(std::vector& games, bool AddGlobalSettings, SDL_Texture* texture) { + games.clear(); + if (AddGlobalSettings) { + Game global; + global.title = "Global"; + global.iconTexture = texture; + global.focusState = false; + games.push_back(global); + } + for (const auto& installLoc : EmulatorSettings.GetAllGameInstallDirs()) { if (installLoc.enabled && std::filesystem::exists(installLoc.path)) { for (const auto& entry : std::filesystem::directory_iterator(installLoc.path)) { @@ -311,12 +355,6 @@ void GetGameInfo() { } Game game; - game.ebootPath = entry.path() / "eboot.bin"; - - const std::string iconFileName = "icon0.png"; - std::filesystem::path iconPath = UpdateChecker(iconFileName, entry.path()); - game.iconTexture = IMG_LoadTexture(renderer, iconPath.string().c_str()); - PSF psf; const std::string sfoFileName = "param.sfo"; std::filesystem::path sfoPath = UpdateChecker(sfoFileName, entry.path()); @@ -325,15 +363,63 @@ void GetGameInfo() { if (const auto title = psf.GetString("TITLE"); title.has_value()) { game.title = *title; } + + if (const auto title_id = psf.GetString("TITLE_ID"); title_id.has_value()) { + game.serial = *title_id; + } } else { continue; } - gameVec.push_back(game); - focusState.push_back(false); + const std::string iconFileName = "icon0.png"; + std::filesystem::path iconPath = UpdateChecker(iconFileName, entry.path()); + LoadTextureDataFromFile(iconPath, game.iconTexture, renderer); + + game.ebootPath = entry.path() / "eboot.bin"; + game.focusState = false; + games.push_back(game); } } } + + // Keep global settings at the start if it's added + auto start = AddGlobalSettings ? games.begin() + 1 : (games.begin()); + std::sort(start, games.end(), [](const Game& a, const Game& b) { + return a.title < b.title; // Alphabetical order + }); +} + +void LoadTextureDataFromFile(std::filesystem::path filePath, SDL_Texture*& texture, + SDL_Renderer* m_renderer) { + std::ifstream file(filePath, std::ios::binary); + std::vector data = + std::vector(std::istreambuf_iterator(file), std::istreambuf_iterator()); + LoadTextureData(data, texture, m_renderer); +} + +void LoadTextureData(std::vector data, SDL_Texture*& texture, SDL_Renderer* m_renderer) { + int image_width = 0; + int image_height = 0; + int channels = 4; + unsigned char* image_data = stbi_load_from_memory( + (const unsigned char*)data.data(), (int)data.size(), &image_width, &image_height, NULL, 4); + if (image_data == nullptr) { + LOG_ERROR(ImGui, "Failed to load image: {}", stbi_failure_reason()); + } + + SDL_Surface* surface = SDL_CreateSurfaceFrom(image_width, image_height, SDL_PIXELFORMAT_RGBA32, + (void*)image_data, channels * image_width); + if (surface == nullptr) { + LOG_ERROR(ImGui, "Unable to create SDL surface: {}", SDL_GetError()); + } + + texture = SDL_CreateTextureFromSurface(m_renderer, surface); + if (texture == nullptr) { + LOG_ERROR(ImGui, "Unable to create SDL texture: {}", SDL_GetError()); + } + + SDL_DestroySurface(surface); + stbi_image_free(image_data); } } // namespace BigPictureMode diff --git a/src/imgui/big_picture.h b/src/imgui/big_picture.h index 1d5b45f73..b5f035541 100644 --- a/src/imgui/big_picture.h +++ b/src/imgui/big_picture.h @@ -4,7 +4,7 @@ #pragma once #include -#include +#include namespace BigPictureMode { @@ -12,11 +12,17 @@ struct Game { SDL_Texture* iconTexture; std::filesystem::path ebootPath; std::string title; + std::string serial; + bool focusState; }; void Launch(); -void SetGameIcons(); -void GetGameInfo(); +void SetGameIcons(std::vector& games); +void GetGameInfo(std::vector& games, bool AddGlobalSettings, SDL_Texture* texture = {}); std::filesystem::path UpdateChecker(const std::string sceItem, std::filesystem::path game_folder); +void LoadTextureDataFromFile(std::filesystem::path filePath, SDL_Texture*& texture, + SDL_Renderer* renderer); +void LoadTextureData(std::vector data, SDL_Texture*& texture, SDL_Renderer* renderer); + } // namespace BigPictureMode diff --git a/src/imgui/settings_dialog_imgui.cpp b/src/imgui/settings_dialog_imgui.cpp new file mode 100644 index 000000000..f55a482a2 --- /dev/null +++ b/src/imgui/settings_dialog_imgui.cpp @@ -0,0 +1,602 @@ +// SPDX-FileCopyrightText: Copyright 2025 shadPS4 Emulator Project +// SPDX-License-Identifier: GPL-2.0-or-later + +#include +#include + +#include "core/devtools/layer.h" +#include "core/emulator_settings.h" +#include "imgui/imgui_std.h" +#include "settings_dialog_imgui.h" + +#include "imgui_fonts/notosansjp_regular.ttf.g.cpp" +#include "imgui_fonts/proggyvector_regular.ttf.g.cpp" + +CMRC_DECLARE(res); + +namespace BigPictureMode { + +const float gameImageSize = 200.f; +const float settingsIconSize = 125.f; +static std::vector settingsProfileVec = {}; + +static float uiScale = 1.0f; +static CurrentSettings currentSettings; +static Textures textures; +static SDL_Renderer* renderer; + +static SettingsCategory currentCategory = SettingsCategory::Profiles; +static std::string currentProfile = "Global"; +static bool closeOnSave = false; + +void Init() { + currentProfile = "Global"; + currentCategory = SettingsCategory::Profiles; + LoadSettings("Global"); + + SDL_Window* window = SDL_GetKeyboardFocus(); + renderer = SDL_GetRenderer(window); + + LoadEmbeddedTexture("src/images/big_picture/settings.png", textures.general); + LoadEmbeddedTexture("src/images/big_picture/folder.png", textures.profiles); + LoadEmbeddedTexture("src/images/big_picture/global-settings.png", textures.globalSettings); + LoadEmbeddedTexture("src/images/big_picture/experimental.png", textures.experimental); + LoadEmbeddedTexture("src/images/big_picture/graphics.png", textures.graphics); + LoadEmbeddedTexture("src/images/big_picture/controller.png", textures.input); + LoadEmbeddedTexture("src/images/big_picture/trophy.png", textures.trophy); + LoadEmbeddedTexture("src/images/big_picture/log.png", textures.log); + + GetGameInfo(settingsProfileVec, true, textures.globalSettings); + uiScale = static_cast(EmulatorSettings.GetBigPictureScale() / 1000.f); +} + +void DeInit() { + EmulatorSettings.Load(); + EmulatorSettings.SetBigPictureScale(static_cast(uiScale * 1000)); + EmulatorSettings.Save(); +} + +void DrawSettings(bool* open) { + if (!*open) + return; + + const ImGuiViewport* viewport = ImGui::GetMainViewport(); + ImGui::SetNextWindowPos(viewport->WorkPos); + ImGui::SetNextWindowSize(viewport->WorkSize); + + if (ImGui::Begin("Settings", nullptr, ImGuiWindowFlags_NoDecoration)) { + if (ImGui::IsWindowAppearing()) { + Init(); + closeOnSave = false; + } + + ImGui::DrawPrettyBackground(); + ImGui::SetWindowFontScale(uiScale); + ImGuiWindowFlags child_flags = ImGuiWindowFlags_NoTitleBar | ImGuiWindowFlags_NoResize | + ImGuiWindowFlags_NoCollapse | ImGuiWindowFlags_NavFlattened; + + ImVec4 settingsColor = ImVec4(0.1f, 0.1f, 0.12f, 0.8f); // Darker gray + ImGui::PushStyleColor(ImGuiCol_ChildBg, settingsColor); + ImGui::BeginChild("Categories", ImVec2(0, 0), ImGuiChildFlags_AutoResizeY, + child_flags | ImGuiWindowFlags_HorizontalScrollbar); + + ImGui::PushStyleVar(ImGuiStyleVar_ItemSpacing, ImVec2(30.0f * uiScale, 0.0f)); + + // Must add categories in enum order for L1/R1 to work correctly, with experimental last + AddCategory("Profiles", textures.profiles, SettingsCategory::Profiles); + AddCategory("General", textures.general, SettingsCategory::General); + AddCategory("Graphics", textures.graphics, SettingsCategory::Graphics); + AddCategory("Input", textures.input, SettingsCategory::Input); + AddCategory("Trophy", textures.trophy, SettingsCategory::Trophy); + AddCategory("Log", textures.log, SettingsCategory::Log); + + if (currentProfile != "Global") + AddCategory("Experimental", textures.experimental, SettingsCategory::Experimental); + + ImGui::PopStyleVar(); + ImGui::EndChild(); // Categories + + if (ImGui::IsWindowAppearing()) { + ImGui::SetKeyboardFocusHere(); + } + + ImGui::BeginChild("ContentRegion", ImVec2(0, -ImGui::GetFrameHeightWithSpacing()), true, + child_flags); + ImGui::PopStyleColor(); + + LoadCategory(currentCategory); + + ImGui::EndChild(); + ImGui::Separator(); + + ImGui::SetNextItemWidth(300.0f * uiScale); + static float sliderScale2 = 1.0f; + if (ImGui::IsWindowAppearing()) { + sliderScale2 = uiScale; + } + + ImGui::SliderFloat("UI Scale", &sliderScale2, 0.25f, 3.0f); + // Only update when user is not interacting with slider + if (ImGui::IsItemDeactivatedAfterEdit()) { + uiScale = sliderScale2; + } + ImGui::SameLine(); + + // Align buttons right + float buttonsWidth = ImGui::CalcTextSize("Save").x + ImGui::CalcTextSize("Cancel").x + + ImGui::CalcTextSize("Apply").x + + ImGui::GetStyle().FramePadding.x * 6.0f + + ImGui::GetStyle().ItemSpacing.x * 2; + ImGui::SetCursorPosX(ImGui::GetWindowContentRegionMax().x - buttonsWidth); + + if (ImGui::Button("Save")) { + closeOnSave = true; + ImGui::OpenPopup("Save Confirmation"); + } + + ImGui::SameLine(); + if (ImGui::Button("Apply")) { + ImGui::OpenPopup("Save Confirmation"); + } + + ImVec2 center = ImGui::GetMainViewport()->GetCenter(); + ImGui::SetNextWindowPos(center, ImGuiCond_Appearing, ImVec2(0.5f, 0.5f)); + if (ImGui::BeginPopupModal("Save Confirmation", nullptr, + ImGuiWindowFlags_AlwaysAutoResize)) { + ImGui::Text("%s", ("Profile Saved:\n" + currentProfile).c_str()); + ImGui::Separator(); + + if (ImGui::Button("OK", ImVec2(250 * uiScale, 0))) { + std::string profile = currentProfile; + if (currentProfile != "Global") { + profile = currentProfile.substr(0, 9); + } + + SaveSettings(profile); + if (closeOnSave) { + DeInit(); + *open = false; + ImGui::CloseCurrentPopup(); + } else { + ImGui::CloseCurrentPopup(); + } + } + + ImGui::EndPopup(); + } + + ImGui::SameLine(); + if (ImGui::Button("Cancel")) { + DeInit(); + *open = false; + } + + SettingsCategory lastCategory = + currentProfile != "Global" ? SettingsCategory::Experimental : SettingsCategory::Log; + // choose next category with R1 + if (ImGui::IsKeyPressed(ImGuiKey_GamepadR1)) { + int currentIndex = static_cast(currentCategory); + currentCategory == lastCategory + ? currentCategory = static_cast(0) + : currentCategory = static_cast(currentIndex + 1); + } + + // choose previous category with R1 + if (ImGui::IsKeyPressed(ImGuiKey_GamepadL1)) { + int currentIndex = static_cast(currentCategory); + currentIndex == 0 ? currentCategory = lastCategory + : currentCategory = static_cast(currentIndex - 1); + } + } + + ImGui::End(); +} + +void LoadCategory(SettingsCategory category) { + ImGui::TextColored(ImVec4(0.00f, 1.00f, 1.00f, 1.00f), "%s", + ("Selected Profile: " + currentProfile).c_str()); // Dark Blue + ImGui::Dummy(ImVec2(0, 20.f * uiScale)); + ImGui::PushStyleVar(ImGuiStyleVar_CellPadding, ImVec2(4.0f * uiScale, 10.0f * uiScale)); + + if (category == SettingsCategory::General) { + if (ImGui::BeginTable("SettingsTable", 2)) { + ImGui::TableSetupColumn("Name", ImGuiTableColumnFlags_WidthFixed, 500.0f * uiScale); + ImGui::TableSetupColumn("Value"); + + AddSettingCombo("Console Language", currentSettings.consoleLanguage); + AddSettingSliderInt("Volume", currentSettings.volume, 0, 500); + AddSettingBool("Show Splash Screen When Launching Game", currentSettings.showSplash); + AddSettingCombo("Audio Backend", currentSettings.audioBackend); + + ImGui::EndTable(); + } + } else if (category == SettingsCategory::Graphics) { + if (ImGui::BeginTable("SettingsTable", 2)) { + ImGui::TableSetupColumn("Name", ImGuiTableColumnFlags_WidthFixed, 500.0f * uiScale); + ImGui::TableSetupColumn("Value"); + + AddSettingCombo("Display Mode", currentSettings.fullscreenMode); + AddSettingCombo("Present Mode", currentSettings.presentMode); + AddSettingSliderInt("Window Width", currentSettings.windowWidth, 0, 8000); + AddSettingSliderInt("Window Height", currentSettings.windowHeight, 0, 7000); + AddSettingBool("Enable HDR", currentSettings.hdrAllowed); + AddSettingBool("Enable FSR", currentSettings.fsrEnabled); + + if (currentSettings.fsrEnabled) { + AddSettingBool("Enable RCAS", currentSettings.rcasEnabled); + } + + if (currentSettings.rcasEnabled && currentSettings.fsrEnabled) { + AddSettingSliderFloat("RCAS Attenuation", currentSettings.rcasAttenuation, 0.0f, + 3.0f, 3); + } + + ImGui::EndTable(); + } + } else if (category == SettingsCategory::Input) { + if (ImGui::BeginTable("SettingsTable", 2)) { + ImGui::TableSetupColumn("Name", ImGuiTableColumnFlags_WidthFixed, 500.0f * uiScale); + ImGui::TableSetupColumn("Value"); + + AddSettingBool("Enable Motion Controls", currentSettings.motionControls); + AddSettingBool("Enable Background Controller Input", + currentSettings.backgroundController); + AddSettingCombo("Hide Cursor", currentSettings.cursorState); + + if (currentSettings.cursorState == 1) { + AddSettingSliderInt("Hide Cursor Idle Timeout", currentSettings.cursorTimeout, 1, + 10); + } + + ImGui::EndTable(); + } + } else if (category == SettingsCategory::Trophy) { + if (ImGui::BeginTable("SettingsTable", 2)) { + ImGui::TableSetupColumn("Name", ImGuiTableColumnFlags_WidthFixed, 500.0f * uiScale); + ImGui::TableSetupColumn("Value"); + + AddSettingBool("Disable Trophy Notification", currentSettings.trophyPopupDisabled); + if (!currentSettings.trophyPopupDisabled) { + AddSettingCombo("Trophy Notification Position", currentSettings.trophySide); + AddSettingSliderFloat("Trophy Notification Duration", + currentSettings.trophyDuration, 0.f, 10.f, 1); + } + + ImGui::EndTable(); + } + } else if (category == SettingsCategory::Log) { + if (ImGui::BeginTable("SettingsTable", 2)) { + ImGui::TableSetupColumn("Name", ImGuiTableColumnFlags_WidthFixed, 500.0f * uiScale); + ImGui::TableSetupColumn("Value"); + + AddSettingBool("Enable Logging", currentSettings.logEnabled); + if (currentSettings.logEnabled) { + AddSettingBool("Separate Log Files", currentSettings.separateLog); + AddSettingCombo("Log Type", currentSettings.logType); + } + + ImGui::EndTable(); + } + } else if (category == SettingsCategory::Experimental) { + if (ImGui::BeginTable("SettingsTable", 2)) { + ImGui::TableSetupColumn("Name", ImGuiTableColumnFlags_WidthFixed, 500.0f * uiScale); + ImGui::TableSetupColumn("Value"); + + AddSettingSliderInt("Additional DMem Allocation", currentSettings.extraDmem, 0, 20000); + AddSettingSliderInt("Vblank Frequency", currentSettings.vblankFrequency, 30, 360); + AddSettingCombo("Readbacks Mode", currentSettings.readbacksMode); + AddSettingBool("Enable Readback Linear Images", currentSettings.readbackLinearImages); + AddSettingBool("Enable Direct Memory Access", currentSettings.directMemoryAccess); + AddSettingBool("Enable Devkit Console Mode", currentSettings.devkitConsole); + AddSettingBool("Enable PS4 Neo Mode", currentSettings.neoMode); + AddSettingBool("Set PSN Sign-in to True", currentSettings.psnSignedIn); + AddSettingBool("Set Network Connected to True", currentSettings.connectedNetwork); + AddSettingBool("Enable Shader Cache", currentSettings.pipelineCacheEnabled); + + if (currentSettings.pipelineCacheEnabled) { + AddSettingBool("Compress Shader Cache to Zip File", + currentSettings.pipelineCacheArchive); + } + + ImGui::EndTable(); + } + } + + ImGui::PopStyleVar(); + + // Child Window if Needed + if (category == SettingsCategory::Profiles) { + ImGuiWindowFlags child_flags = ImGuiWindowFlags_NoTitleBar | ImGuiWindowFlags_NoResize | + ImGuiWindowFlags_NoCollapse | ImGuiWindowFlags_NavFlattened; + ImGui::BeginChild("ProfileSelect", ImVec2(0, 0), true, child_flags); + Overlay::TextCentered("Select Global or Game-Specific Settings Profile"); + SetProfileIcons(settingsProfileVec); + ImGui::EndChild(); + } +} + +void SaveSettings(std::string profile) { + const bool isSpecific = currentProfile != "Global"; + + /////////// General Tab + EmulatorSettings.SetConsoleLanguage( + languageMap.at(optionsLanguage.at(currentSettings.consoleLanguage)), isSpecific); + EmulatorSettings.SetVolumeSlider(currentSettings.volume, isSpecific); + EmulatorSettings.SetShowSplash(currentSettings.showSplash, isSpecific); + EmulatorSettings.SetAudioBackend(currentSettings.audioBackend, isSpecific); + + /////////// Graphics Tab + bool isFullscreen = currentSettings.fullscreenMode != 0; + EmulatorSettings.SetFullScreen(isFullscreen); + EmulatorSettings.SetFullScreenMode(optionsFullscreenMode.at(currentSettings.fullscreenMode), + isSpecific); + EmulatorSettings.SetPresentMode(optionsPresentMode.at(currentSettings.presentMode), isSpecific); + EmulatorSettings.SetWindowHeight(currentSettings.windowHeight, isSpecific); + EmulatorSettings.SetWindowWidth(currentSettings.windowWidth, isSpecific); + EmulatorSettings.SetHdrAllowed(currentSettings.hdrAllowed, isSpecific); + EmulatorSettings.SetFsrEnabled(currentSettings.fsrEnabled, isSpecific); + EmulatorSettings.SetRcasEnabled(currentSettings.rcasEnabled, isSpecific); + EmulatorSettings.SetRcasAttenuation(static_cast(currentSettings.rcasAttenuation * 1000), + isSpecific); + + /////////// Input Tab + EmulatorSettings.SetMotionControlsEnabled(currentSettings.motionControls, isSpecific); + EmulatorSettings.SetBackgroundControllerInput(currentSettings.backgroundController, isSpecific); + EmulatorSettings.SetCursorState(currentSettings.cursorState, isSpecific); + EmulatorSettings.SetCursorHideTimeout(currentSettings.cursorTimeout, isSpecific); + + /////////// Trophy Tab + EmulatorSettings.SetTrophyPopupDisabled(currentSettings.trophyPopupDisabled, isSpecific); + EmulatorSettings.SetTrophyNotificationSide(optionsTrophySide.at(currentSettings.trophySide), + isSpecific); + EmulatorSettings.SetTrophyNotificationDuration( + static_cast(currentSettings.trophyDuration)); + + /////////// Log Tab + EmulatorSettings.SetLogEnabled(currentSettings.logEnabled, isSpecific); + EmulatorSettings.SetLogType(optionsLogType.at(currentSettings.logType), isSpecific); + EmulatorSettings.SetSeparateLoggingEnabled(currentSettings.separateLog, isSpecific); + + /////////// Experimental Tab + if (isSpecific) { + EmulatorSettings.SetReadbacksMode(currentSettings.readbacksMode, true); + EmulatorSettings.SetReadbackLinearImagesEnabled(currentSettings.readbackLinearImages, true); + EmulatorSettings.SetDirectMemoryAccessEnabled(currentSettings.directMemoryAccess, true); + EmulatorSettings.SetDevKit(currentSettings.devkitConsole, true); + EmulatorSettings.SetNeo(currentSettings.neoMode, true); + EmulatorSettings.SetPSNSignedIn(currentSettings.psnSignedIn, true); + EmulatorSettings.SetConnectedToNetwork(currentSettings.connectedNetwork, true); + EmulatorSettings.SetPipelineCacheEnabled(currentSettings.pipelineCacheEnabled, true); + EmulatorSettings.SetPipelineCacheArchived(currentSettings.pipelineCacheArchive, true); + EmulatorSettings.SetExtraDmemInMBytes(currentSettings.extraDmem, true); + EmulatorSettings.SetVblankFrequency(currentSettings.vblankFrequency, true); + } + + if (!isSpecific) { + EmulatorSettings.Save(); + } else { + EmulatorSettings.Save(profile); + } +} + +void LoadSettings(std::string profile) { + const bool isSpecific = currentProfile != "Global"; + if (!isSpecific) { + EmulatorSettings.Load(); + } else { + EmulatorSettings.Load(profile); + } + + /////////// General Tab + int languageIndex = EmulatorSettings.GetConsoleLanguage(); + std::string language; + for (const auto& [key, value] : languageMap) { + if (value == languageIndex) { + language = key; + } + } + currentSettings.consoleLanguage = GetComboIndex(language, optionsLanguage); + currentSettings.volume = EmulatorSettings.GetVolumeSlider(); + currentSettings.showSplash = EmulatorSettings.IsShowSplash(); + currentSettings.audioBackend = EmulatorSettings.GetAudioBackend(); + + /////////// Graphics Tab + currentSettings.fullscreenMode = + GetComboIndex(EmulatorSettings.GetFullScreenMode(), optionsFullscreenMode); + currentSettings.presentMode = + GetComboIndex(EmulatorSettings.GetPresentMode(), optionsPresentMode); + currentSettings.windowHeight = EmulatorSettings.GetWindowHeight(); + currentSettings.windowWidth = EmulatorSettings.GetWindowWidth(); + currentSettings.hdrAllowed = EmulatorSettings.IsHdrAllowed(); + currentSettings.fsrEnabled = EmulatorSettings.IsFsrEnabled(); + currentSettings.rcasEnabled = EmulatorSettings.IsRcasEnabled(); + currentSettings.rcasAttenuation = + static_cast(EmulatorSettings.GetRcasAttenuation() * 0.001f); + + /////////// Input Tab + currentSettings.motionControls = EmulatorSettings.IsMotionControlsEnabled(); + currentSettings.backgroundController = EmulatorSettings.IsBackgroundControllerInput(); + currentSettings.cursorState = EmulatorSettings.GetCursorState(); + currentSettings.cursorTimeout = EmulatorSettings.GetCursorHideTimeout(); + + /////////// Trophy Tab + currentSettings.trophyPopupDisabled = EmulatorSettings.IsTrophyPopupDisabled(); + currentSettings.trophySide = + GetComboIndex(EmulatorSettings.GetTrophyNotificationSide(), optionsTrophySide); + currentSettings.trophyDuration = + static_cast(EmulatorSettings.GetTrophyNotificationDuration()); + + /////////// Log Tab + currentSettings.logEnabled = EmulatorSettings.IsLogEnabled(); + currentSettings.logType = GetComboIndex(EmulatorSettings.GetLogType(), optionsLogType); + currentSettings.separateLog = EmulatorSettings.IsSeparateLoggingEnabled(); + + /////////// Experimental Tab + if (isSpecific) { + currentSettings.readbacksMode = EmulatorSettings.GetReadbacksMode(); + currentSettings.readbackLinearImages = EmulatorSettings.IsReadbackLinearImagesEnabled(); + currentSettings.directMemoryAccess = EmulatorSettings.IsDirectMemoryAccessEnabled(); + currentSettings.devkitConsole = EmulatorSettings.IsDevKit(); + currentSettings.neoMode = EmulatorSettings.IsNeo(); + currentSettings.psnSignedIn = EmulatorSettings.IsPSNSignedIn(); + currentSettings.connectedNetwork = EmulatorSettings.IsConnectedToNetwork(); + currentSettings.pipelineCacheEnabled = EmulatorSettings.IsPipelineCacheEnabled(); + currentSettings.pipelineCacheArchive = EmulatorSettings.IsPipelineCacheArchived(); + currentSettings.extraDmem = EmulatorSettings.GetExtraDmemInMBytes(); + currentSettings.vblankFrequency = EmulatorSettings.GetVblankFrequency(); + } +} + +void AddCategory(std::string name, SDL_Texture* texture, SettingsCategory category) { + ImGui::SameLine(); + ImGui::BeginGroup(); + + // make button appear hovered as long as category is selected, otherwise dull it's hovered color + currentCategory == category + ? ImGui::PushStyleColor(ImGuiCol_Button, ImGui::GetStyle().Colors[ImGuiCol_ButtonHovered]) + : ImGui::PushStyleColor(ImGuiCol_ButtonHovered, ImVec4(0.235f, 0.392f, 0.624f, 1.00f)); + + if (ImGui::ImageButton(name.c_str(), ImTextureID(texture), + ImVec2(settingsIconSize * uiScale, settingsIconSize * uiScale))) { + currentCategory = category; + } + + ImGui::PopStyleColor(); + + ImGui::SetCursorPosX( + (ImGui::GetCursorPosX() + + (settingsIconSize * uiScale - ImGui::CalcTextSize(name.c_str()).x) * 0.5f) + + ImGui::GetStyle().FramePadding.x); + ImGui::Text("%s", name.c_str()); + ImGui::EndGroup(); +} + +void AddSettingBool(std::string name, bool& value) { + std::string label = "##" + name; + ImGui::TableNextRow(); + ImGui::TableNextColumn(); + + ImGui::TextWrapped("%s", name.c_str()); + ImGui::TableNextColumn(); + ImGui::Checkbox(label.c_str(), &value); +} + +void AddSettingSliderInt(std::string name, int& value, int min, int max) { + std::string label = "##" + name; + ImGui::TableNextRow(); + ImGui::TableNextColumn(); + ImGui::TextWrapped("%s", name.c_str()); + + ImGui::TableNextColumn(); + ImGui::SliderInt(label.c_str(), &value, min, max); +} + +void AddSettingSliderFloat(std::string name, float& value, int min, int max, int precision) { + std::string label = "##" + name; + std::string precisionString = "%." + std::to_string(precision) + "f"; + + ImGui::TableNextRow(); + ImGui::TableNextColumn(); + ImGui::TextWrapped("%s", name.c_str()); + + ImGui::TableNextColumn(); + ImGui::SliderFloat(label.c_str(), &value, min, max, precisionString.c_str()); +} + +void AddSettingCombo(std::string name, int& value) { + std::string label = "##" + name; + ImGui::TableNextRow(); + ImGui::TableNextColumn(); + ImGui::TextWrapped("%s", name.c_str()); + + std::vector options = optionsMap.at(name); + ImGui::TableNextColumn(); + + const char* combo_value = options[value].c_str(); + if (ImGui::BeginCombo(label.c_str(), combo_value)) { + for (int i = 0; i < options.size(); i++) { + const bool selected = (i == value); + if (ImGui::Selectable(options[i].c_str(), selected)) + value = i; + + // Set the initial focus when opening the combo + if (selected) + ImGui::SetItemDefaultFocus(); + } + ImGui::EndCombo(); + } +} + +int GetComboIndex(std::string selection, std::vector options) { + for (int i = 0; i < options.size(); i++) { + if (selection == options[i]) + return i; + } + + return 0; +} + +void LoadEmbeddedTexture(std::string resourcePath, SDL_Texture*& texture) { + auto resource = cmrc::res::get_filesystem(); + auto file = resource.open(resourcePath); + std::vector texData = std::vector(file.begin(), file.end()); + + BigPictureMode::LoadTextureData(texData, texture, renderer); +} + +void SetProfileIcons(std::vector& games) { + ImGuiStyle& style = ImGui::GetStyle(); + const float maxAvailableWidth = ImGui::GetContentRegionAvail().x; + const float itemSpacing = style.ItemSpacing.x; // already scaled + const float padding = 10.0f * uiScale; + float rowContentWidth = gameImageSize * uiScale + itemSpacing; + + for (int i = 0; i < games.size(); i++) { + ImGui::BeginGroup(); + std::string ButtonName = "Button" + std::to_string(i); + const char* ButtonNameChar = ButtonName.c_str(); + + bool isNextItemFocused = (ImGui::GetID(ButtonNameChar) == ImGui::GetFocusID()); + bool popColor = false; + if (isNextItemFocused) { + ImGui::PushStyleColor(ImGuiCol_Button, + ImGui::GetStyle().Colors[ImGuiCol_ButtonHovered]); + popColor = true; + } + + if (ImGui::ImageButton(ButtonNameChar, (ImTextureID)games[i].iconTexture, + ImVec2(gameImageSize * uiScale, gameImageSize * uiScale))) { + currentProfile = i == 0 ? "Global" : games[i].serial + " - " + games[i].title; + LoadSettings(games[i].serial); + } + + if (popColor) { + ImGui::PopStyleColor(); + } + + // Scroll to item only when newly-focused + if (ImGui::IsItemFocused() && !games[i].focusState) { + ImGui::SetScrollHereY(0.5f); + } + + if (ImGui::IsWindowFocused()) { + games[i].focusState = ImGui::IsItemFocused(); + } + + ImGui::PushTextWrapPos(ImGui::GetCursorPos().x + gameImageSize * uiScale); + ImGui::TextWrapped("%s", games[i].title.c_str()); + ImGui::PopTextWrapPos(); + ImGui::EndGroup(); + + // Use same line if content fits horizontally, move to next line if not + rowContentWidth += (gameImageSize * uiScale + itemSpacing * 2 + padding); + if (rowContentWidth < maxAvailableWidth) { + ImGui::SameLine(0.0f, padding); + } else { + ImGui::Dummy(ImVec2(0.0f, padding)); + rowContentWidth = gameImageSize * uiScale + itemSpacing; + } + } +} + +} // namespace BigPictureMode diff --git a/src/imgui/settings_dialog_imgui.h b/src/imgui/settings_dialog_imgui.h new file mode 100644 index 000000000..0686a8406 --- /dev/null +++ b/src/imgui/settings_dialog_imgui.h @@ -0,0 +1,191 @@ +// SPDX-FileCopyrightText: Copyright 2025 shadPS4 Emulator Project +// SPDX-License-Identifier: GPL-2.0-or-later + +#pragma once + +#include +#include +#include +#include +#include + +#include "big_picture.h" + +namespace BigPictureMode { + +enum class SettingsCategory { + Profiles, + General, + Graphics, + Input, + Trophy, + Log, + Experimental, +}; + +struct Textures { + SDL_Texture* profiles; + SDL_Texture* general; + SDL_Texture* globalSettings; + SDL_Texture* experimental; + SDL_Texture* graphics; + SDL_Texture* input; + SDL_Texture* trophy; + SDL_Texture* log; +}; + +void Init(); +void DeInit(); + +void SetProfileIcons(std::vector& games); +void LoadEmbeddedTexture(std::string resourcePath, SDL_Texture*& texture); +void AddCategory(std::string name, SDL_Texture* texture, SettingsCategory category); + +void DrawSettings(bool* open); +void SaveSettings(std::string profile); +void LoadSettings(std::string profile); +void LoadCategory(SettingsCategory); + +void AddSettingBool(std::string name, bool& value); +void AddSettingSliderInt(std::string name, int& value, int min, int max); +void AddSettingSliderFloat(std::string name, float& value, int min, int max, int precision); +void AddSettingCombo(std::string name, int& value); +int GetComboIndex(std::string selection, std::vector options); + +//////////////////// Settings struct +// Note: use int instead of std::string for all combo settings as needed by ImGui +// then convert to string when saving/loading + +struct CurrentSettings { + // General tab + int consoleLanguage; + int volume; + bool showSplash; + int audioBackend; + + // Graphics tab + int fullscreenMode; + int presentMode; + int windowWidth; + int windowHeight; + bool hdrAllowed; + bool fsrEnabled; + bool rcasEnabled; + float rcasAttenuation; + + // Input tab + bool motionControls; + bool backgroundController; + int cursorState; + int cursorTimeout; + + // Trophy tab + bool trophyPopupDisabled; + int trophySide; + float trophyDuration; + + // Log tab + bool logEnabled; + bool separateLog; + int logType; + + // Experimental tab + int readbacksMode; + bool readbackLinearImages; + bool directMemoryAccess; + bool devkitConsole; + bool neoMode; + bool psnSignedIn; + bool connectedNetwork; + bool pipelineCacheEnabled; + bool pipelineCacheArchive; + int extraDmem; + int vblankFrequency; +}; + +//////////////////// option maps for comboboxes and other needed constants +const std::map languageMap = {{"Arabic", 21}, + {"Czech", 23}, + {"Danish", 14}, + {"Dutch", 6}, + {"English (United Kingdom)", 18}, + {"English (United States)", 1}, + {"Finnish", 12}, + {"French (Canada)", 22}, + {"French (France)", 2}, + {"German", 4}, + {"Greek", 25}, + {"Hungarian", 24}, + {"Indonesian", 29}, + {"Italian", 5}, + {"Japanese", 0}, + {"Korean", 9}, + {"Norwegian (Bokmaal)", 15}, + {"Polish", 16}, + {"Portuguese (Brazil)", 17}, + {"Portuguese (Portugal)", 7}, + {"Romanian", 26}, + {"Russian", 8}, + {"Simplified Chinese", 11}, + {"Spanish (Latin America)", 20}, + {"Spanish (Spain)", 3}, + {"Swedish", 13}, + {"Thai", 27}, + {"Traditional Chinese", 10}, + {"Turkish", 19}, + {"Ukrainian", 30}, + {"Vietnamese", 28}}; + +const std::vector optionsLanguage = {"Arabic", + "Czech", + "Danish", + "Dutch", + "English (United Kingdom)", + "English (United States)", + "Finnish", + "French (Canada)", + "French (France)", + "German", + "Greek", + "Hungarian", + "Indonesian", + "Italian", + "Japanese", + "Korean", + "Norwegian (Bokmaal)", + "Polish", + "Portuguese (Brazil)", + "Portuguese (Portugal)", + "Romanian", + "Russian", + "Simplified Chinese", + "Spanish (Latin America)", + "Spanish (Spain)", + "Swedish", + "Thai", + "Traditional Chinese", + "Turkish", + "Ukrainian", + "Vietnamese"}; + +const std::vector optionsLogType = {"sync", "async"}; +const std::vector optionsFullscreenMode = {"Windowed", "Fullscreen", + "Fullscreen (Borderless)"}; +const std::vector optionsAudioBackend = {"SDL", "OpenAL"}; +const std::vector optionsPresentMode = {"Mailbox", "Fifo", "Immediate"}; +const std::vector optionsHideCursor = {"Never", "Idle", "Always"}; +const std::vector optionsTrophySide = {"left", "right", "top", "bottom"}; +const std::vector optionsReadbacksMode = {"Disabled", "Relaxed", "Precise"}; + +const std::map> optionsMap = { + {"Log Type", optionsLogType}, + {"Console Language", optionsLanguage}, + {"Audio Backend", optionsAudioBackend}, + {"Display Mode", optionsFullscreenMode}, + {"Present Mode", optionsPresentMode}, + {"Hide Cursor", optionsHideCursor}, + {"Trophy Notification Position", optionsTrophySide}, + {"Readbacks Mode", optionsReadbacksMode}, +}; + +} // namespace BigPictureMode