diff --git a/.gitignore b/.gitignore index 43ab8ae..682effd 100644 --- a/.gitignore +++ b/.gitignore @@ -1,6 +1,9 @@ # cmake files /build +# cargo files +/ui/target + # ide/lsp files /.zed /.vscode diff --git a/README.md b/README.md index bddfc79..4fe55c1 100644 --- a/README.md +++ b/README.md @@ -14,5 +14,7 @@ curl -sSf https://pancake.gay/lsfg-vk.sh | sh Please see the [Wiki](https://github.com/PancakeTAS/lsfg-vk/wiki) for more information and join the [Discord](https://discord.gg/losslessscaling) for help (In order to see the linux channels, verify your Steam account.) +Thanks to @Caliel666 for writing the GTK-based gui for lsfg-vk! + >[!WARNING] > **Please do not open GitHub** issues for anything other than feature requests. Due to the nature of this project, it is much easier to deal with issues through Discord, than GitHub. Use the #linux-reports channel for game compatibility. diff --git a/ui/Cargo.lock b/ui/Cargo.lock new file mode 100644 index 0000000..fb3b967 --- /dev/null +++ b/ui/Cargo.lock @@ -0,0 +1,952 @@ +# This file is automatically @generated by Cargo. +# It is not intended for manual editing. +version = 4 + +[[package]] +name = "anyhow" +version = "1.0.98" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e16d2d3311acee920a9eb8d33b8cbc1787ce4a264e85f964c2404b969bdcd487" + +[[package]] +name = "autocfg" +version = "1.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c08606f8c3cbf4ce6ec8e28fb0014a2c086708fe954eaa885384a6165172e7e8" + +[[package]] +name = "bitflags" +version = "2.9.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1b8e56985ec62d17e9c1001dc89c88ecd7dc08e47eba5ec7c29c7b5eeecde967" + +[[package]] +name = "cairo-rs" +version = "0.18.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8ca26ef0159422fb77631dc9d17b102f253b876fe1586b03b803e63a309b4ee2" +dependencies = [ + "bitflags", + "cairo-sys-rs", + "glib", + "libc", + "once_cell", + "thiserror", +] + +[[package]] +name = "cairo-sys-rs" +version = "0.18.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "685c9fa8e590b8b3d678873528d83411db17242a73fccaed827770ea0fedda51" +dependencies = [ + "glib-sys", + "libc", + "system-deps", +] + +[[package]] +name = "cfg-expr" +version = "0.15.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d067ad48b8650848b989a59a86c6c36a995d02d2bf778d45c3c5d57bc2718f02" +dependencies = [ + "smallvec", + "target-lexicon", +] + +[[package]] +name = "cfg-if" +version = "1.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9555578bc9e57714c812a1f84e4fc5b4d21fcb063490c624de019f7464c91268" + +[[package]] +name = "directories" +version = "5.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9a49173b84e034382284f27f1af4dcbbd231ffa358c0fe316541a7337f376a35" +dependencies = [ + "dirs-sys", +] + +[[package]] +name = "dirs" +version = "5.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "44c45a9d03d6676652bcb5e724c7e988de1acad23a711b5217ab9cbecbec2225" +dependencies = [ + "dirs-sys", +] + +[[package]] +name = "dirs-sys" +version = "0.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "520f05a5cbd335fae5a99ff7a6ab8627577660ee5cfd6a94a6a929b52ff0321c" +dependencies = [ + "libc", + "option-ext", + "redox_users", + "windows-sys", +] + +[[package]] +name = "equivalent" +version = "1.0.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "877a4ace8713b0bcf2a4e7eec82529c029f1d0619886d18145fea96c3ffe5c0f" + +[[package]] +name = "field-offset" +version = "0.3.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "38e2275cc4e4fc009b0669731a1e5ab7ebf11f469eaede2bab9309a5b4d6057f" +dependencies = [ + "memoffset", + "rustc_version", +] + +[[package]] +name = "futures-channel" +version = "0.3.31" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2dff15bf788c671c1934e366d07e30c1814a8ef514e1af724a602e8a2fbe1b10" +dependencies = [ + "futures-core", +] + +[[package]] +name = "futures-core" +version = "0.3.31" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "05f29059c0c2090612e8d742178b0580d2dc940c837851ad723096f87af6663e" + +[[package]] +name = "futures-executor" +version = "0.3.31" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1e28d1d997f585e54aebc3f97d39e72338912123a67330d723fdbb564d646c9f" +dependencies = [ + "futures-core", + "futures-task", + "futures-util", +] + +[[package]] +name = "futures-io" +version = "0.3.31" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9e5c1b78ca4aae1ac06c48a526a655760685149f0d465d21f37abfe57ce075c6" + +[[package]] +name = "futures-macro" +version = "0.3.31" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "162ee34ebcb7c64a8abebc059ce0fee27c2262618d7b60ed8faf72fef13c3650" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.104", +] + +[[package]] +name = "futures-task" +version = "0.3.31" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f90f7dce0722e95104fcb095585910c0977252f286e354b5e3bd38902cd99988" + +[[package]] +name = "futures-util" +version = "0.3.31" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9fa08315bb612088cc391249efdc3bc77536f16c91f6cf495e6fbe85b20a4a81" +dependencies = [ + "futures-core", + "futures-macro", + "futures-task", + "pin-project-lite", + "pin-utils", + "slab", +] + +[[package]] +name = "gdk-pixbuf" +version = "0.18.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "50e1f5f1b0bfb830d6ccc8066d18db35c487b1b2b1e8589b5dfe9f07e8defaec" +dependencies = [ + "gdk-pixbuf-sys", + "gio", + "glib", + "libc", + "once_cell", +] + +[[package]] +name = "gdk-pixbuf-sys" +version = "0.18.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3f9839ea644ed9c97a34d129ad56d38a25e6756f99f3a88e15cd39c20629caf7" +dependencies = [ + "gio-sys", + "glib-sys", + "gobject-sys", + "libc", + "system-deps", +] + +[[package]] +name = "gdk4" +version = "0.7.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7edb019ad581f8ecf8ea8e4baa6df7c483a95b5a59be3140be6a9c3b0c632af6" +dependencies = [ + "cairo-rs", + "gdk-pixbuf", + "gdk4-sys", + "gio", + "glib", + "libc", + "pango", +] + +[[package]] +name = "gdk4-sys" +version = "0.7.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dbab43f332a3cf1df9974da690b5bb0e26720ed09a228178ce52175372dcfef0" +dependencies = [ + "cairo-sys-rs", + "gdk-pixbuf-sys", + "gio-sys", + "glib-sys", + "gobject-sys", + "libc", + "pango-sys", + "pkg-config", + "system-deps", +] + +[[package]] +name = "getrandom" +version = "0.2.16" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "335ff9f135e4384c8150d6f27c6daed433577f86b4750418338c01a1a2528592" +dependencies = [ + "cfg-if", + "libc", + "wasi", +] + +[[package]] +name = "gio" +version = "0.18.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d4fc8f532f87b79cbc51a79748f16a6828fb784be93145a322fa14d06d354c73" +dependencies = [ + "futures-channel", + "futures-core", + "futures-io", + "futures-util", + "gio-sys", + "glib", + "libc", + "once_cell", + "pin-project-lite", + "smallvec", + "thiserror", +] + +[[package]] +name = "gio-sys" +version = "0.18.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "37566df850baf5e4cb0dfb78af2e4b9898d817ed9263d1090a2df958c64737d2" +dependencies = [ + "glib-sys", + "gobject-sys", + "libc", + "system-deps", + "winapi", +] + +[[package]] +name = "glib" +version = "0.18.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "233daaf6e83ae6a12a52055f568f9d7cf4671dabb78ff9560ab6da230ce00ee5" +dependencies = [ + "bitflags", + "futures-channel", + "futures-core", + "futures-executor", + "futures-task", + "futures-util", + "gio-sys", + "glib-macros", + "glib-sys", + "gobject-sys", + "libc", + "memchr", + "once_cell", + "smallvec", + "thiserror", +] + +[[package]] +name = "glib-macros" +version = "0.18.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0bb0228f477c0900c880fd78c8759b95c7636dbd7842707f49e132378aa2acdc" +dependencies = [ + "heck 0.4.1", + "proc-macro-crate 2.0.2", + "proc-macro-error", + "proc-macro2", + "quote", + "syn 2.0.104", +] + +[[package]] +name = "glib-sys" +version = "0.18.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "063ce2eb6a8d0ea93d2bf8ba1957e78dbab6be1c2220dd3daca57d5a9d869898" +dependencies = [ + "libc", + "system-deps", +] + +[[package]] +name = "gobject-sys" +version = "0.18.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0850127b514d1c4a4654ead6dedadb18198999985908e6ffe4436f53c785ce44" +dependencies = [ + "glib-sys", + "libc", + "system-deps", +] + +[[package]] +name = "graphene-rs" +version = "0.18.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3b2228cda1505613a7a956cca69076892cfbda84fc2b7a62b94a41a272c0c401" +dependencies = [ + "glib", + "graphene-sys", + "libc", +] + +[[package]] +name = "graphene-sys" +version = "0.18.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cc4144cee8fc8788f2a9b73dc5f1d4e1189d1f95305c4cb7bd9c1af1cfa31f59" +dependencies = [ + "glib-sys", + "libc", + "pkg-config", + "system-deps", +] + +[[package]] +name = "gsk4" +version = "0.7.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0d958e351d2f210309b32d081c832d7de0aca0b077aa10d88336c6379bd01f7e" +dependencies = [ + "cairo-rs", + "gdk4", + "glib", + "graphene-rs", + "gsk4-sys", + "libc", + "pango", +] + +[[package]] +name = "gsk4-sys" +version = "0.7.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "12bd9e3effea989f020e8f1ff3fa3b8c63ba93d43b899c11a118868853a56d55" +dependencies = [ + "cairo-sys-rs", + "gdk4-sys", + "glib-sys", + "gobject-sys", + "graphene-sys", + "libc", + "pango-sys", + "system-deps", +] + +[[package]] +name = "gtk4" +version = "0.7.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5aeb51aa3e9728575a053e1f43543cd9992ac2477e1b186ad824fd4adfb70842" +dependencies = [ + "cairo-rs", + "field-offset", + "futures-channel", + "gdk-pixbuf", + "gdk4", + "gio", + "glib", + "graphene-rs", + "gsk4", + "gtk4-macros", + "gtk4-sys", + "libc", + "pango", +] + +[[package]] +name = "gtk4-macros" +version = "0.7.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d57ec49cf9b657f69a05bca8027cff0a8dfd0c49e812be026fc7311f2163832f" +dependencies = [ + "anyhow", + "proc-macro-crate 1.3.1", + "proc-macro-error", + "proc-macro2", + "quote", + "syn 1.0.109", +] + +[[package]] +name = "gtk4-sys" +version = "0.7.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "54d8c4aa23638ce9faa2caf7e2a27d4a1295af2155c8e8d28c4d4eeca7a65eb8" +dependencies = [ + "cairo-sys-rs", + "gdk-pixbuf-sys", + "gdk4-sys", + "gio-sys", + "glib-sys", + "gobject-sys", + "graphene-sys", + "gsk4-sys", + "libc", + "pango-sys", + "system-deps", +] + +[[package]] +name = "hashbrown" +version = "0.15.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5971ac85611da7067dbfcabef3c70ebb5606018acd9e2a3903a0da507521e0d5" + +[[package]] +name = "heck" +version = "0.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "95505c38b4572b2d910cecb0281560f54b440a19336cbbcb27bf6ce6adc6f5a8" + +[[package]] +name = "heck" +version = "0.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2304e00983f87ffb38b55b444b5e3b60a884b5d30c0fca7d82fe33449bbe55ea" + +[[package]] +name = "indexmap" +version = "2.10.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fe4cd85333e22411419a0bcae1297d25e58c9443848b11dc6a86fefe8c78a661" +dependencies = [ + "equivalent", + "hashbrown", +] + +[[package]] +name = "libadwaita" +version = "0.5.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2fe7e70c06507ed10a16cda707f358fbe60fe0dc237498f78c686ade92fd979c" +dependencies = [ + "gdk-pixbuf", + "gdk4", + "gio", + "glib", + "gtk4", + "libadwaita-sys", + "libc", + "pango", +] + +[[package]] +name = "libadwaita-sys" +version = "0.5.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5e10aaa38de1d53374f90deeb4535209adc40cc5dba37f9704724169bceec69a" +dependencies = [ + "gdk4-sys", + "gio-sys", + "glib-sys", + "gobject-sys", + "gtk4-sys", + "libc", + "pango-sys", + "system-deps", +] + +[[package]] +name = "libc" +version = "0.2.174" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1171693293099992e19cddea4e8b849964e9846f4acee11b3948bcc337be8776" + +[[package]] +name = "libredox" +version = "0.1.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4488594b9328dee448adb906d8b126d9b7deb7cf5c22161ee591610bb1be83c0" +dependencies = [ + "bitflags", + "libc", +] + +[[package]] +name = "lsfg-vk-ui" +version = "0.1.3" +dependencies = [ + "directories", + "dirs", + "gtk4", + "libadwaita", + "serde", + "toml", +] + +[[package]] +name = "memchr" +version = "2.7.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "32a282da65faaf38286cf3be983213fcf1d2e2a58700e808f83f4ea9a4804bc0" + +[[package]] +name = "memoffset" +version = "0.9.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "488016bfae457b036d996092f6cb448677611ce4449e970ceaf42695203f218a" +dependencies = [ + "autocfg", +] + +[[package]] +name = "once_cell" +version = "1.21.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "42f5e15c9953c5e4ccceeb2e7382a716482c34515315f7b03532b8b4e8393d2d" + +[[package]] +name = "option-ext" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "04744f49eae99ab78e0d5c0b603ab218f515ea8cfe5a456d7629ad883a3b6e7d" + +[[package]] +name = "pango" +version = "0.18.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7ca27ec1eb0457ab26f3036ea52229edbdb74dee1edd29063f5b9b010e7ebee4" +dependencies = [ + "gio", + "glib", + "libc", + "once_cell", + "pango-sys", +] + +[[package]] +name = "pango-sys" +version = "0.18.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "436737e391a843e5933d6d9aa102cb126d501e815b83601365a948a518555dc5" +dependencies = [ + "glib-sys", + "gobject-sys", + "libc", + "system-deps", +] + +[[package]] +name = "pin-project-lite" +version = "0.2.16" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3b3cff922bd51709b605d9ead9aa71031d81447142d828eb4a6eba76fe619f9b" + +[[package]] +name = "pin-utils" +version = "0.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8b870d8c151b6f2fb93e84a13146138f05d02ed11c7e7c54f8826aaaf7c9f184" + +[[package]] +name = "pkg-config" +version = "0.3.32" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7edddbd0b52d732b21ad9a5fab5c704c14cd949e5e9a1ec5929a24fded1b904c" + +[[package]] +name = "proc-macro-crate" +version = "1.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7f4c021e1093a56626774e81216a4ce732a735e5bad4868a03f3ed65ca0c3919" +dependencies = [ + "once_cell", + "toml_edit 0.19.15", +] + +[[package]] +name = "proc-macro-crate" +version = "2.0.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b00f26d3400549137f92511a46ac1cd8ce37cb5598a96d382381458b992a5d24" +dependencies = [ + "toml_datetime", + "toml_edit 0.20.2", +] + +[[package]] +name = "proc-macro-error" +version = "1.0.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "da25490ff9892aab3fcf7c36f08cfb902dd3e71ca0f9f9517bea02a73a5ce38c" +dependencies = [ + "proc-macro-error-attr", + "proc-macro2", + "quote", + "syn 1.0.109", + "version_check", +] + +[[package]] +name = "proc-macro-error-attr" +version = "1.0.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a1be40180e52ecc98ad80b184934baf3d0d29f979574e439af5a55274b35f869" +dependencies = [ + "proc-macro2", + "quote", + "version_check", +] + +[[package]] +name = "proc-macro2" +version = "1.0.95" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "02b3e5e68a3a1a02aad3ec490a98007cbc13c37cbe84a3cd7b8e406d76e7f778" +dependencies = [ + "unicode-ident", +] + +[[package]] +name = "quote" +version = "1.0.40" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1885c039570dc00dcb4ff087a89e185fd56bae234ddc7f056a945bf36467248d" +dependencies = [ + "proc-macro2", +] + +[[package]] +name = "redox_users" +version = "0.4.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ba009ff324d1fc1b900bd1fdb31564febe58a8ccc8a6fdbb93b543d33b13ca43" +dependencies = [ + "getrandom", + "libredox", + "thiserror", +] + +[[package]] +name = "rustc_version" +version = "0.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cfcb3a22ef46e85b45de6ee7e79d063319ebb6594faafcf1c225ea92ab6e9b92" +dependencies = [ + "semver", +] + +[[package]] +name = "semver" +version = "1.0.26" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "56e6fa9c48d24d85fb3de5ad847117517440f6beceb7798af16b4a87d616b8d0" + +[[package]] +name = "serde" +version = "1.0.219" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5f0e2c6ed6606019b4e29e69dbaba95b11854410e5347d525002456dbbb786b6" +dependencies = [ + "serde_derive", +] + +[[package]] +name = "serde_derive" +version = "1.0.219" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5b0276cf7f2c73365f7157c8123c21cd9a50fbbd844757af28ca1f5925fc2a00" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.104", +] + +[[package]] +name = "serde_spanned" +version = "0.6.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bf41e0cfaf7226dca15e8197172c295a782857fcb97fad1808a166870dee75a3" +dependencies = [ + "serde", +] + +[[package]] +name = "slab" +version = "0.4.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "04dc19736151f35336d325007ac991178d504a119863a2fcb3758cdb5e52c50d" + +[[package]] +name = "smallvec" +version = "1.15.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "67b1b7a3b5fe4f1376887184045fcf45c69e92af734b7aaddc05fb777b6fbd03" + +[[package]] +name = "syn" +version = "1.0.109" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "72b64191b275b66ffe2469e8af2c1cfe3bafa67b529ead792a6d0160888b4237" +dependencies = [ + "proc-macro2", + "quote", + "unicode-ident", +] + +[[package]] +name = "syn" +version = "2.0.104" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "17b6f705963418cdb9927482fa304bc562ece2fdd4f616084c50b7023b435a40" +dependencies = [ + "proc-macro2", + "quote", + "unicode-ident", +] + +[[package]] +name = "system-deps" +version = "6.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a3e535eb8dded36d55ec13eddacd30dec501792ff23a0b1682c38601b8cf2349" +dependencies = [ + "cfg-expr", + "heck 0.5.0", + "pkg-config", + "toml", + "version-compare", +] + +[[package]] +name = "target-lexicon" +version = "0.12.16" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "61c41af27dd6d1e27b1b16b489db798443478cef1f06a660c96db617ba5de3b1" + +[[package]] +name = "thiserror" +version = "1.0.69" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b6aaf5339b578ea85b50e080feb250a3e8ae8cfcdff9a461c9ec2904bc923f52" +dependencies = [ + "thiserror-impl", +] + +[[package]] +name = "thiserror-impl" +version = "1.0.69" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4fee6c4efc90059e10f81e6d42c60a18f76588c3d74cb83a0b242a2b6c7504c1" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.104", +] + +[[package]] +name = "toml" +version = "0.8.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "185d8ab0dfbb35cf1399a6344d8484209c088f75f8f68230da55d48d95d43e3d" +dependencies = [ + "serde", + "serde_spanned", + "toml_datetime", + "toml_edit 0.20.2", +] + +[[package]] +name = "toml_datetime" +version = "0.6.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7cda73e2f1397b1262d6dfdcef8aafae14d1de7748d66822d3bfeeb6d03e5e4b" +dependencies = [ + "serde", +] + +[[package]] +name = "toml_edit" +version = "0.19.15" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1b5bb770da30e5cbfde35a2d7b9b8a2c4b8ef89548a7a6aeab5c9a576e3e7421" +dependencies = [ + "indexmap", + "toml_datetime", + "winnow", +] + +[[package]] +name = "toml_edit" +version = "0.20.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "396e4d48bbb2b7554c944bde63101b5ae446cff6ec4a24227428f15eb72ef338" +dependencies = [ + "indexmap", + "serde", + "serde_spanned", + "toml_datetime", + "winnow", +] + +[[package]] +name = "unicode-ident" +version = "1.0.18" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5a5f39404a5da50712a4c1eecf25e90dd62b613502b7e925fd4e4d19b5c96512" + +[[package]] +name = "version-compare" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "852e951cb7832cb45cb1169900d19760cfa39b82bc0ea9c0e5a14ae88411c98b" + +[[package]] +name = "version_check" +version = "0.9.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0b928f33d975fc6ad9f86c8f283853ad26bdd5b10b7f1542aa2fa15e2289105a" + +[[package]] +name = "wasi" +version = "0.11.1+wasi-snapshot-preview1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ccf3ec651a847eb01de73ccad15eb7d99f80485de043efb2f370cd654f4ea44b" + +[[package]] +name = "winapi" +version = "0.3.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5c839a674fcd7a98952e593242ea400abe93992746761e38641405d28b00f419" +dependencies = [ + "winapi-i686-pc-windows-gnu", + "winapi-x86_64-pc-windows-gnu", +] + +[[package]] +name = "winapi-i686-pc-windows-gnu" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ac3b87c63620426dd9b991e5ce0329eff545bccbbb34f3be09ff6fb6ab51b7b6" + +[[package]] +name = "winapi-x86_64-pc-windows-gnu" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "712e227841d057c1ee1cd2fb22fa7e5a5461ae8e48fa2ca79ec42cfc1931183f" + +[[package]] +name = "windows-sys" +version = "0.48.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "677d2418bec65e3338edb076e806bc1ec15693c5d0104683f2efe857f61056a9" +dependencies = [ + "windows-targets", +] + +[[package]] +name = "windows-targets" +version = "0.48.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9a2fa6e2155d7247be68c096456083145c183cbbbc2764150dda45a87197940c" +dependencies = [ + "windows_aarch64_gnullvm", + "windows_aarch64_msvc", + "windows_i686_gnu", + "windows_i686_msvc", + "windows_x86_64_gnu", + "windows_x86_64_gnullvm", + "windows_x86_64_msvc", +] + +[[package]] +name = "windows_aarch64_gnullvm" +version = "0.48.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2b38e32f0abccf9987a4e3079dfb67dcd799fb61361e53e2882c3cbaf0d905d8" + +[[package]] +name = "windows_aarch64_msvc" +version = "0.48.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dc35310971f3b2dbbf3f0690a219f40e2d9afcf64f9ab7cc1be722937c26b4bc" + +[[package]] +name = "windows_i686_gnu" +version = "0.48.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a75915e7def60c94dcef72200b9a8e58e5091744960da64ec734a6c6e9b3743e" + +[[package]] +name = "windows_i686_msvc" +version = "0.48.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8f55c233f70c4b27f66c523580f78f1004e8b5a8b659e05a4eb49d4166cca406" + +[[package]] +name = "windows_x86_64_gnu" +version = "0.48.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "53d40abd2583d23e4718fddf1ebec84dbff8381c07cae67ff7768bbf19c6718e" + +[[package]] +name = "windows_x86_64_gnullvm" +version = "0.48.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0b7b52767868a23d5bab768e390dc5f5c55825b6d30b86c844ff2dc7414044cc" + +[[package]] +name = "windows_x86_64_msvc" +version = "0.48.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ed94fce61571a4006852b7389a063ab983c02eb1bb37b47f8272ce92d06d9538" + +[[package]] +name = "winnow" +version = "0.5.40" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f593a95398737aeed53e489c785df13f3618e41dbcd6718c6addbf1395aa6876" +dependencies = [ + "memchr", +] diff --git a/ui/Cargo.toml b/ui/Cargo.toml new file mode 100644 index 0000000..3524dea --- /dev/null +++ b/ui/Cargo.toml @@ -0,0 +1,14 @@ +[package] +name = "lsfg-vk-ui" +version = "0.1.3" +edition = "2021" +authors = ["Cali666"] +description = "Lossless Scaling Frame Generation Configuration Tool" + +[dependencies] +libadwaita = "0.5" +gtk = { version = "0.7", package = "gtk4" } +directories = "5.0" +serde = { version = "1.0", features = ["derive"] } +toml = "0.8" +dirs = "5.0" diff --git a/ui/resources/com.cali666.lsfg-vk-ui.desktop b/ui/resources/com.cali666.lsfg-vk-ui.desktop new file mode 100644 index 0000000..640a161 --- /dev/null +++ b/ui/resources/com.cali666.lsfg-vk-ui.desktop @@ -0,0 +1,13 @@ +[Desktop Entry] +Version=1.0 +Type=Application +Name=LSFG-VK UI +Comment=Lossless Scaling Frame Generation Configuration Tool +Exec=lsfg-vk-ui %U +Icon=com.cali666.lsfg-vk-ui +Terminal=false +Categories=Game;Settings; +Keywords=lossless;scaling;frame;generation;gaming;graphics;configuration; +StartupNotify=true +StartupWMClass=com.cali666.lsfg-vk-ui +MimeType=application/x-lsfg-profile; diff --git a/ui/resources/icons/lsfg-vk.png b/ui/resources/icons/lsfg-vk.png new file mode 100644 index 0000000..1c67ee2 Binary files /dev/null and b/ui/resources/icons/lsfg-vk.png differ diff --git a/ui/resources/ui.ui b/ui/resources/ui.ui new file mode 100644 index 0000000..d72e763 --- /dev/null +++ b/ui/resources/ui.ui @@ -0,0 +1,347 @@ + + + + + Lossless Scaling Frame Generation + 800 + 600 + true + + + + + + + Lossless Scaling Frame Generation + + + + + + Settings + settings-icon-button + + + + + + + + horizontal + + + + never + automatic + + + vertical + 0 + sidebar-content + + + LSFG Profiles + 0.0 + 12 + 12 + 6 + title-1 + + + + + browse + navigation-sidebar + + + + + + Create New Profile + 12 + 12 + 12 + suggested-action + + + + + + + + + + vertical + true + true + + + center + + + + + true + true + + + + settings_page + Settings + + + never + + + vertical + 48 + 48 + 32 + 32 + 32 + + + + + Frame Generation + true + 8 + 8 + + + true + + + horizontal + 16 + center + 12 + 12 + 8 + 8 + + + Multiplier + start + true + 0 + + + + + + + + off + 2 + 3 + 4 + + + + 0 + + + + + + + + + true + + + horizontal + 16 + center + 12 + 12 + 8 + 8 + + + Flow Scale + start + true + 0 + + + + + 0.7 + number + + + + + + + + + true + + + horizontal + 16 + center + 12 + 12 + 8 + 8 + + + Performance Mode + start + true + 0 + + + + + true + compact + + + + + + + + + + + + + Misc + true + 8 + 8 + + + true + + + horizontal + 16 + center + 12 + 12 + 8 + 8 + + + HDR Mode + start + true + 0 + + + + + true + compact + + + + + + + + + true + + + horizontal + 16 + center + 12 + 12 + 8 + 8 + + + Experimental Present Mode + start + true + 0 + + + + + + + + vsync + mailbox + immediate + + + + 0 + + + + + + + + + + + + + + + + + + about_page + About + + + vertical + center + center + 24 + + + LSFG-VK UI + title-1 + + + + + Lossless Scaling Frame Generation Configuration Tool + title-4 + true + 50 + + + + + Made by Cali666 • 2025 + dim-label + 24 + + + + + For more information refer to the lsfg-vk wiki + https://github.com/PancakeTAS/lsfg-vk/wiki + 12 + + + + + + + + + + + + + + + + diff --git a/ui/src/app_state.rs b/ui/src/app_state.rs new file mode 100644 index 0000000..552da38 --- /dev/null +++ b/ui/src/app_state.rs @@ -0,0 +1,293 @@ +use gtk::prelude::*; +use gtk::{glib, MessageDialog}; +use libadwaita::ApplicationWindow; +use std::cell::RefCell; +use std::rc::Rc; + +use crate::config::{Config, save_config}; +use crate::utils::round_to_2_decimals; + +#[allow(dead_code)] +pub struct AppState { + pub config: Config, + pub selected_profile_index: Option, + // Store references to the UI widgets for easy access and updates + pub main_window: ApplicationWindow, + pub sidebar_list_box: gtk::ListBox, + pub multiplier_dropdown: gtk::DropDown, + pub flow_scale_entry: gtk::Entry, + pub performance_mode_switch: gtk::Switch, + pub hdr_mode_switch: gtk::Switch, + pub experimental_present_mode_dropdown: gtk::DropDown, + pub save_button: gtk::Button, + pub main_settings_box: gtk::Box, + pub main_stack: gtk::Stack, + // Store SignalHandlerIds to block/unblock signals + pub multiplier_dropdown_handler_id: Option, + pub flow_scale_entry_handler_id: Option, + pub performance_mode_switch_handler_id: Option, + pub hdr_mode_switch_handler_id: Option, + pub experimental_present_mode_dropdown_handler_id: Option, +} + +impl AppState { + // Saves the current configuration to the TOML file + pub fn save_current_config(&self) { + if let Err(e) = save_config(&self.config) { + eprintln!("Failed to save config: {}", e); + // In a real app, you'd show a user-friendly error dialog here + } + } + + // Updates the main window UI with data from the currently selected profile + pub fn update_main_window_from_profile(&self) { + if let Some(index) = self.selected_profile_index { + if let Some(profile) = self.config.game.get(index) { + // Temporarily block signals to prevent re-entrancy + let _guard_mult = self.multiplier_dropdown_handler_id.as_ref().map(|id| self.multiplier_dropdown.block_signal(id)); + let _guard_flow = self.flow_scale_entry_handler_id.as_ref().map(|id| self.flow_scale_entry.block_signal(id)); + let _guard_perf = self.performance_mode_switch_handler_id.as_ref().map(|id| self.performance_mode_switch.block_signal(id)); + let _guard_hdr = self.hdr_mode_switch_handler_id.as_ref().map(|id| self.hdr_mode_switch.block_signal(id)); + let _guard_exp = self.experimental_present_mode_dropdown_handler_id.as_ref().map(|id| self.experimental_present_mode_dropdown.block_signal(id)); + + // Update Multiplier Dropdown + let multiplier_str = match profile.multiplier { + 1 => "off".to_string(), + _ => profile.multiplier.to_string(), + }; + if let Some(pos) = self.multiplier_dropdown.model().and_then(|model| { + let list_model = model.downcast_ref::()?; + (0..list_model.n_items()).find(|&i| list_model.string(i).map_or(false, |s| s.as_str() == multiplier_str)) + }) { + self.multiplier_dropdown.set_selected(pos); + } + + // Update Flow Scale Entry (round to avoid floating point display issues) + let rounded_flow_scale = round_to_2_decimals(profile.flow_scale); + self.flow_scale_entry.set_text(&format!("{:.2}", rounded_flow_scale)); + + // Update Performance Mode Switch + self.performance_mode_switch.set_active(profile.performance_mode); + + // Update HDR Mode Switch + self.hdr_mode_switch.set_active(profile.hdr_mode); + + // Update Experimental Present Mode Dropdown + if let Some(pos) = self.experimental_present_mode_dropdown.model().and_then(|model| { + let list_model = model.downcast_ref::()?; + (0..list_model.n_items()).find(|&i| list_model.string(i).map_or(false, |s| s.as_str() == profile.experimental_present_mode)) + }) { + self.experimental_present_mode_dropdown.set_selected(pos); + } + // Signal handlers are unblocked automatically when _guard_X go out of scope + + // Switch to the settings page + self.main_stack.set_visible_child_name("settings_page"); + + } + } else { + // Clear or disable main window elements if no profile is selected + self.multiplier_dropdown.set_selected(0); // Default to 'off' or first item + self.flow_scale_entry.set_text(""); + self.performance_mode_switch.set_active(false); + self.hdr_mode_switch.set_active(false); + self.experimental_present_mode_dropdown.set_selected(0); // Default to first item + + // Switch to the about page + self.main_stack.set_visible_child_name("about_page"); + } + } + + // Populates sidebar with optional app_state for button handlers + pub fn populate_sidebar_with_handlers(&self, app_state: Option>>) { + // Clear existing rows + while let Some(child) = self.sidebar_list_box.first_child() { + self.sidebar_list_box.remove(&child); + } + + let mut row_to_select: Option = None; + + for (i, profile) in self.config.game.iter().enumerate() { + let row = gtk::ListBoxRow::new(); + + // Create a horizontal box to hold the profile name and buttons + let row_box = gtk::Box::builder() + .orientation(gtk::Orientation::Horizontal) + .spacing(8) + .margin_start(12) + .margin_end(12) + .margin_top(8) + .margin_bottom(8) + .build(); + + // Profile name label + let label = gtk::Label::builder() + .label(&profile.exe) + .halign(gtk::Align::Start) + .hexpand(true) + .build(); + + // Edit button + let edit_button = gtk::Button::builder() + .label("🖊") + .css_classes(["flat", "circular"]) + .tooltip_text("Edit profile name") + .build(); + + // Remove button + let remove_button = gtk::Button::builder() + .label("𐄂") + .css_classes(["flat", "circular", "destructive-action"]) + .tooltip_text("Remove profile") + .build(); + + // Add all elements to the row box + row_box.append(&label); + row_box.append(&edit_button); + row_box.append(&remove_button); + + // Connect button handlers if app_state is available + if let Some(app_state_ref) = &app_state { + // Edit button handler + let app_state_clone = app_state_ref.clone(); + let profile_index = i; + edit_button.connect_clicked(move |_| { + let state = app_state_clone.borrow(); + let main_window = &state.main_window; + + // Create edit dialog + let dialog = MessageDialog::new( + Some(main_window), + gtk::DialogFlags::MODAL, + gtk::MessageType::Question, + gtk::ButtonsType::None, + "Edit profile name:", + ); + dialog.set_title(Some("Edit Profile")); + + let entry = gtk::Entry::builder() + .placeholder_text("Profile Name") + .text(&state.config.game[profile_index].exe) + .margin_top(12) + .margin_bottom(12) + .margin_start(12) + .margin_end(12) + .build(); + + dialog.content_area().append(&entry); + dialog.add_button("Cancel", gtk::ResponseType::Cancel); + dialog.add_button("Save", gtk::ResponseType::Other(1)); + dialog.set_default_response(gtk::ResponseType::Other(1)); + + // Allow pressing Enter in the entry to trigger the "Save" button + let dialog_clone = dialog.clone(); + entry.connect_activate(move |_| { + dialog_clone.response(gtk::ResponseType::Other(1)); + }); + + let app_state_clone_dialog = app_state_clone.clone(); + let entry_clone = entry.clone(); + dialog.connect_response(move |d, response| { + if response == gtk::ResponseType::Other(1) { + let new_name = entry_clone.text().to_string(); + if !new_name.is_empty() { + let mut state = app_state_clone_dialog.borrow_mut(); + + // Check if profile with this name already exists (excluding current) + if state.config.game.iter().enumerate().any(|(idx, p)| idx != profile_index && p.exe == new_name) { + let error_dialog = MessageDialog::new( + Some(d), + gtk::DialogFlags::MODAL, + gtk::MessageType::Error, + gtk::ButtonsType::Ok, + "A profile with this name already exists", + ); + error_dialog.set_title(Some("Error")); + error_dialog.connect_response(move |d, _| { d.close(); }); + error_dialog.present(); + return; + } + + // Update profile name + state.config.game[profile_index].exe = new_name; + state.save_current_config(); + state.populate_sidebar_with_handlers(Some(app_state_clone_dialog.clone())); + } + } + d.close(); + }); + dialog.present(); + }); + + // Remove button handler + let app_state_clone = app_state_ref.clone(); + let profile_index = i; + remove_button.connect_clicked(move |_| { + let state = app_state_clone.borrow(); + let main_window = &state.main_window; + let profile_name = &state.config.game[profile_index].exe; + + // Create confirmation dialog + let dialog = MessageDialog::new( + Some(main_window), + gtk::DialogFlags::MODAL, + gtk::MessageType::Warning, + gtk::ButtonsType::None, + &format!("Are you sure you want to remove the profile '{}'?", profile_name), + ); + dialog.set_title(Some("Remove Profile")); + dialog.add_button("Cancel", gtk::ResponseType::Cancel); + dialog.add_button("Remove", gtk::ResponseType::Other(1)); + dialog.set_default_response(gtk::ResponseType::Cancel); + + let app_state_clone_dialog = app_state_clone.clone(); + dialog.connect_response(move |d, response| { + if response == gtk::ResponseType::Other(1) { + let mut state = app_state_clone_dialog.borrow_mut(); + + // Remove the profile + state.config.game.remove(profile_index); + + // Update selected index if needed + if let Some(selected) = state.selected_profile_index { + if selected == profile_index { + // If we removed the selected profile, select the first available or none + state.selected_profile_index = if state.config.game.is_empty() { None } else { Some(0) }; + } else if selected > profile_index { + // Adjust index if we removed a profile before the selected one + state.selected_profile_index = Some(selected - 1); + } + } + + state.save_current_config(); + state.populate_sidebar_with_handlers(Some(app_state_clone_dialog.clone())); + drop(state); + + // Update main window + app_state_clone_dialog.borrow().update_main_window_from_profile(); + } + d.close(); + }); + dialog.present(); + }); + } + + row.set_child(Some(&row_box)); + self.sidebar_list_box.append(&row); + + // Mark the row to be selected later + if self.selected_profile_index == Some(i) { + row_to_select = Some(row.clone()); // Clone the row to store it + } + } + + // Perform selection in a separate idle callback + if let Some(row) = row_to_select { + let list_box_clone = self.sidebar_list_box.clone(); + glib::idle_add_local(move || { + list_box_clone.select_row(Some(&row)); + glib::ControlFlow::Break + }); + } + } +} diff --git a/ui/src/config.rs b/ui/src/config.rs new file mode 100644 index 0000000..7fdde70 --- /dev/null +++ b/ui/src/config.rs @@ -0,0 +1,141 @@ +use serde::{Deserialize, Serialize}; +use std::{fs, io}; +use std::path::PathBuf; +use toml; +use dirs; + +use crate::utils::round_to_2_decimals; // Import from utils module + +// --- Configuration Data Structures --- + +#[derive(Debug, Default, Serialize, Deserialize, Clone)] +pub struct Config { + pub version: u32, // Made public to be accessible from main.rs + #[serde(flatten)] // Flatten this struct into the parent, controlling order + pub ordered_global: OrderedGlobalConfig, + #[serde(default)] + pub game: Vec, +} + +// Helper struct to control the serialization order of global config +#[derive(Debug, Default, Serialize, Deserialize, Clone)] +pub struct OrderedGlobalConfig { + #[serde(default, skip_serializing_if = "Option::is_none")] // Only serialize if Some + pub global: Option, +} + +#[derive(Debug, Default, Serialize, Deserialize, Clone)] +pub struct GlobalConfig { + #[serde(default, skip_serializing_if = "Option::is_none")] // Only serialize if Some + pub dll: Option, +} + +#[derive(Debug, Serialize, Deserialize, Clone)] +#[serde(default)] +pub struct GameProfile { + pub exe: String, + pub multiplier: u32, + #[serde(serialize_with = "serialize_flow_scale", deserialize_with = "deserialize_flow_scale")] + pub flow_scale: f32, + pub performance_mode: bool, + pub hdr_mode: bool, + pub experimental_present_mode: String, +} + +// Default values for a new game profile +impl Default for GameProfile { + fn default() -> Self { + GameProfile { + exe: String::new(), + multiplier: 1, // Default to "off" (1) + flow_scale: round_to_2_decimals(0.7), + performance_mode: true, + hdr_mode: false, + experimental_present_mode: "vsync".to_string(), + } + } +} + +// Custom serde functions to ensure flow_scale is always rounded +fn serialize_flow_scale(value: &f32, serializer: S) -> Result +where + S: serde::Serializer, +{ + // Force to 2 decimal places and serialize as a precise decimal + let rounded = round_to_2_decimals(*value); + let formatted = format!("{:.2}", rounded); + let precise_value: f64 = formatted.parse().unwrap_or(*value as f64); + serializer.serialize_f64(precise_value) +} + +fn deserialize_flow_scale<'de, D>(deserializer: D) -> Result +where + D: serde::Deserializer<'de>, +{ + use serde::Deserialize; + let value = f64::deserialize(deserializer)?; + Ok(round_to_2_decimals(value as f32)) +} + +// --- Configuration File Handling Functions --- + +pub fn get_config_path() -> Result { + let config_dir = dirs::config_dir() + .ok_or_else(|| io::Error::new(io::ErrorKind::NotFound, "Could not find config directory"))? + .join("lsfg-vk"); + + fs::create_dir_all(&config_dir)?; // Ensure directory exists + println!("Config directory: {:?}", config_dir); + Ok(config_dir.join("conf.toml")) +} + + +pub fn load_config() -> Result { + let config_path = get_config_path()?; + println!("Attempting to load config from: {:?}", config_path); + if config_path.exists() { + let contents = fs::read_to_string(&config_path)?; + println!("Successfully read config contents ({} bytes).", contents.len()); + // Load configuration with default values when the format is invalid + let mut config: Config = toml::from_str(&contents).unwrap_or_else(|_| Config::default()); + + // Old way to load config + // let mut config: Config = toml::from_str(&contents).map_err(|e| { + // io::Error::new( + // io::ErrorKind::InvalidData, + // format!("Failed to parse TOML: {}", e), + // ) + // })?; + + + // Clean up any floating point precision issues in existing configs + let mut needs_save = false; + for profile in &mut config.game { + let original = profile.flow_scale; + profile.flow_scale = round_to_2_decimals(profile.flow_scale); + if (original - profile.flow_scale).abs() > f32::EPSILON { + needs_save = true; + } + } + + // Save the cleaned config if we made changes + if needs_save { + let _ = save_config(&config); + } + + Ok(config) + } else { + println!("Config file not found at {:?}, creating default.", config_path); + Ok(Config { version: 1, ordered_global: OrderedGlobalConfig { global: None }, game: Vec::new() }) + } +} + +pub fn save_config(config: &Config) -> Result<(), io::Error> { + let config_path = get_config_path()?; + println!("Attempting to save config to: {:?}", config_path); + let toml_string = toml::to_string_pretty(config) + .map_err(|e| io::Error::new(io::ErrorKind::Other, format!("Failed to serialize TOML: {}", e)))?; + fs::write(&config_path, toml_string)?; + println!("Successfully saved config."); + Ok(()) +} diff --git a/ui/src/main.rs b/ui/src/main.rs new file mode 100644 index 0000000..f3d066e --- /dev/null +++ b/ui/src/main.rs @@ -0,0 +1,497 @@ +use gtk::prelude::*; +use gtk::{glib, CssProvider, Builder, Label}; +use libadwaita::ApplicationWindow; +use libadwaita::prelude::AdwApplicationWindowExt; +use std::cell::RefCell; +use std::rc::Rc; + +// Import modules +mod config; +mod app_state; +mod utils; +mod settings_window; + +use config::load_config; +use app_state::AppState; +use utils::round_to_2_decimals; +use config::OrderedGlobalConfig; + +fn main() -> glib::ExitCode { + let application = libadwaita::Application::builder() + .application_id("com.cali666.lsfg-vk-ui") + .build(); + + // Set the desktop file name for proper GNOME integration + glib::set_application_name("LSFG-VK UI"); + glib::set_prgname(Some("lsfg-vk-ui")); + + application.connect_startup(move |_app| { + // Load CSS for sidebar background + let provider = CssProvider::new(); + provider.load_from_data(&format!( + ".settings-icon-button {{ + font-size: 1.4rem; + }} + + .sidebar {{ + background-color: @theme_bg_color; + }} + + .sidebar-content {{ + background-color: shade(@theme_bg_color, {}); + color: @theme_fg_color; + padding: 12px; + }}\n + .linked-button-box {{ + margin-top: 12px; + margin-bottom: 12px; + }}", + 0.95 + )); + gtk::style_context_add_provider_for_display( + >k::gdk::Display::default().expect("Could not connect to a display."), + &provider, + gtk::STYLE_PROVIDER_PRIORITY_APPLICATION, + ); + + // Set up icon theme for the application icon + if let Some(display) = gtk::gdk::Display::default() { + let icon_theme = gtk::IconTheme::for_display(&display); + icon_theme.add_resource_path("/com/cali666/lsfg-vk-ui/icons"); + } + }); + + application.connect_activate(move |app| { + // Load initial configuration + let initial_config = load_config().unwrap_or_else(|e| { + eprintln!("Error loading config: {}", e); + // Corrected Config initialization + config::Config { version: 1, ordered_global: OrderedGlobalConfig { global: None }, game: Vec::new() } + }); + + // Load UI from .ui file + let ui_bytes = include_bytes!("../resources/ui.ui"); + let builder = Builder::from_string(std::str::from_utf8(ui_bytes).unwrap()); + + // Get main window and other widgets + let main_window: ApplicationWindow = builder + .object("main_window") + .expect("Could not get main_window from builder"); + main_window.set_application(Some(app)); + + let settings_button: gtk::Button = builder + .object("settings_button") + .expect("Could not get settings_button from builder"); + + // Set application icon for proper dock integration + main_window.set_icon_name(Some("com.cali666.lsfg-vk-ui")); + + let sidebar_list_box: gtk::ListBox = builder + .object("sidebar_list_box") + .expect("Could not get sidebar_list_box from builder"); + let create_profile_button: gtk::Button = builder + .object("create_profile_button") + .expect("Could not get create_profile_button from builder"); + + let multiplier_dropdown: gtk::DropDown = builder + .object("multiplier_dropdown") + .expect("Could not get multiplier_dropdown from builder"); + let flow_scale_entry: gtk::Entry = builder + .object("flow_scale_entry") + .expect("Could not get flow_scale_entry from builder"); + let performance_mode_switch: gtk::Switch = builder + .object("performance_mode_switch") + .expect("Could not get performance_mode_switch from builder"); + let hdr_mode_switch: gtk::Switch = builder + .object("hdr_mode_switch") + .expect("Could not get hdr_mode_switch from builder"); + let experimental_present_mode_dropdown: gtk::DropDown = builder + .object("experimental_present_mode_dropdown") + .expect("Could not get experimental_present_mode_dropdown from builder"); + + let main_stack: gtk::Stack = builder + .object("main_stack") + .expect("Could not get main_stack from builder. Ensure it has id='main_stack' in ui.ui."); + let main_stack_switcher: gtk::StackSwitcher = builder + .object("main_stack_switcher") + .expect("Could not get main_stack_switcher from builder. Ensure it has id='main_stack_switcher' in ui.ui."); + + main_stack_switcher.set_stack(Some(&main_stack)); + + let main_settings_box: gtk::Box = builder + .object("main_box") + .expect("Could not get main_box from builder"); + + let save_button = gtk::Button::builder() + .label("Save Changes") + .halign(gtk::Align::End) + .margin_end(12) + .margin_bottom(12) + .build(); + + main_settings_box.append(&save_button); + + // Initialize application state (with None for handler IDs initially) + let app_state = Rc::new(RefCell::new(AppState { + config: initial_config, + selected_profile_index: None, + main_window: main_window.clone(), + sidebar_list_box: sidebar_list_box.clone(), + multiplier_dropdown: multiplier_dropdown.clone(), + flow_scale_entry: flow_scale_entry.clone(), + performance_mode_switch: performance_mode_switch.clone(), + hdr_mode_switch: hdr_mode_switch.clone(), + experimental_present_mode_dropdown: experimental_present_mode_dropdown.clone(), + save_button: save_button.clone(), + main_settings_box: main_settings_box.clone(), + main_stack: main_stack.clone(), + multiplier_dropdown_handler_id: None, + flow_scale_entry_handler_id: None, + performance_mode_switch_handler_id: None, + hdr_mode_switch_handler_id: None, + experimental_present_mode_dropdown_handler_id: None, + })); + + // --- Connect Signals --- + + // Connect settings button + let main_window_clone = main_window.clone(); + let app_state_clone_for_settings = app_state.clone(); // Clone for settings window + settings_button.connect_clicked(move |_| { + let settings_win = settings_window::create_settings_window(&main_window_clone, app_state_clone_for_settings.clone()); + settings_win.present(); + }); + + let app_state_clone = app_state.clone(); + sidebar_list_box.connect_row_activated(move |_list_box, row| { + let index = row.index() as usize; + let mut state = app_state_clone.borrow_mut(); + state.selected_profile_index = Some(index); + drop(state); + + let app_state_for_idle = app_state_clone.clone(); + glib::idle_add_local(move || { + app_state_for_idle.borrow().update_main_window_from_profile(); + glib::ControlFlow::Break + }); + }); + + let app_state_clone = app_state.clone(); + create_profile_button.connect_clicked(move |_| { + let dialog = gtk::MessageDialog::new( + Some(&app_state_clone.borrow().main_window), + gtk::DialogFlags::MODAL, + gtk::MessageType::Question, + gtk::ButtonsType::None, + "", + ); + dialog.set_title(Some("New Profile")); + dialog.set_secondary_text(Some("Enter or browse Application Name")); + + let entry = gtk::Entry::builder() + .placeholder_text("Application Name") + .hexpand(true) + .build(); + + let pick_process_button = gtk::Button::builder() + .label("🖵") + .tooltip_text("Pick a running Vulkan process") + .css_classes(["flat", "square", "icon-button"]) + .build(); + + let entry_box = gtk::Box::builder() + .orientation(gtk::Orientation::Horizontal) + .spacing(6) + .margin_top(12) + .margin_bottom(12) + .margin_start(12) + .margin_end(12) + .build(); + entry_box.append(&entry); + entry_box.append(&pick_process_button); + + dialog.content_area().append(&entry_box); + + dialog.add_button("Cancel", gtk::ResponseType::Cancel); + dialog.add_button("Create", gtk::ResponseType::Other(1)); + + dialog.set_default_response(gtk::ResponseType::Other(1)); + + // Allow pressing Enter in the entry to trigger the "Create" button + let dialog_clone = dialog.clone(); + entry.connect_activate(move |_| { + dialog_clone.response(gtk::ResponseType::Other(1)); + }); + + // --- Process Picker Button Logic --- + let entry_clone_for_picker = entry.clone(); + let main_window_clone_for_picker = app_state_clone.borrow().main_window.clone(); + + pick_process_button.connect_clicked(move |_| { + let process_picker_window = libadwaita::ApplicationWindow::builder() + .title("Select Process") + .transient_for(&main_window_clone_for_picker) + .modal(true) + .default_width(400) + .default_height(600) + .build(); + + let scrolled_window = gtk::ScrolledWindow::builder() + .hscrollbar_policy(gtk::PolicyType::Never) + .vscrollbar_policy(gtk::PolicyType::Automatic) + .hexpand(true) // Make the scrolled window expand horizontally + .vexpand(true) // Make the scrolled window expand vertically + .margin_top(12) + .margin_start(12) + .margin_end(12) + .build(); + + let process_list_box = gtk::ListBox::builder() + .selection_mode(gtk::SelectionMode::Single) + .build(); + scrolled_window.set_child(Some(&process_list_box)); + + let content_box = gtk::Box::builder() + .orientation(gtk::Orientation::Vertical) + .build(); + content_box.append(&scrolled_window); // Add scrolled window first to take up space + + let close_button = gtk::Button::builder() + .label("Close") + .halign(gtk::Align::End) + .margin_end(12) + .margin_bottom(12) + .build(); + content_box.append(&close_button); // Add close button at the bottom + + process_picker_window.set_content(Some(&content_box)); + + // Populate the list with processes + let processes = utils::get_vulkan_processes(); // Call the new function from utils.rs + for proc_name in processes { + let row = gtk::ListBoxRow::new(); + let label = gtk::Label::builder() + .label(&proc_name) + .halign(gtk::Align::Start) + .margin_start(12) + .margin_end(12) + .margin_top(8) + .margin_bottom(8) + .build(); + row.set_child(Some(&label)); + process_list_box.append(&row); + } + + // Connect selection handler + let entry_clone_for_select = entry_clone_for_picker.clone(); + let picker_window_clone = process_picker_window.clone(); + process_list_box.connect_row_activated(move |_list_box, row| { + if let Some(label_widget) = row.child().and_then(|c| c.downcast::().ok()) { + let process_name = label_widget.label().to_string(); + entry_clone_for_select.set_text(&process_name); + picker_window_clone.close(); + } + }); + + // Connect close button + let picker_window_clone_for_close = process_picker_window.clone(); + close_button.connect_clicked(move |_| { + picker_window_clone_for_close.close(); + }); + + process_picker_window.present(); + }); + // --- End Process Picker Button Logic --- + + let app_state_clone_dialog = app_state_clone.clone(); + let entry_clone = entry.clone(); + dialog.connect_response( + move |d: >k::MessageDialog, response: gtk::ResponseType| { + if response == gtk::ResponseType::Other(1) { + let game_name = entry_clone.text().to_string(); + if !game_name.is_empty() { + let mut state = app_state_clone_dialog.borrow_mut(); + + if state.config.game.iter().any(|p| p.exe == game_name) { + let error_dialog = gtk::MessageDialog::new( + Some(d), + gtk::DialogFlags::MODAL, + gtk::MessageType::Error, + gtk::ButtonsType::Ok, + "A profile with this name already exists", + ); + error_dialog.set_title(Some("Error")); + error_dialog.connect_response(move |d, _| { d.close(); }); + error_dialog.present(); + return; + } + + let new_profile = config::GameProfile { + exe: game_name, + ..Default::default() + }; + + state.config.game.push(new_profile); + state.selected_profile_index = Some(state.config.game.len() - 1); + + state.save_current_config(); + + state.populate_sidebar_with_handlers(Some(app_state_clone_dialog.clone())); + drop(state); + + let app_state_for_idle = app_state_clone_dialog.clone(); + glib::idle_add_local(move || { + app_state_for_idle.borrow().update_main_window_from_profile(); + glib::ControlFlow::Break + }); + } + } + d.close(); + } + ); + dialog.present(); + }); + + let app_state_clone_for_handler_mult = app_state.clone(); + let multiplier_handler_id = multiplier_dropdown.connect_selected_item_notify(move |dropdown| { + let mut state = app_state_clone_for_handler_mult.borrow_mut(); + + if let Some(index) = state.selected_profile_index { + if index < state.config.game.len() { + if let Some(profile) = state.config.game.get_mut(index) { + if let Some(item) = dropdown.selected_item() { + if let Some(string_obj) = item.downcast_ref::() { + let text = string_obj.string(); + profile.multiplier = match text.as_str() { + "off" => 1, + _ => text.parse().unwrap_or(1), + }; + } + } + } + } + } + }); + app_state.borrow_mut().multiplier_dropdown_handler_id = Some(multiplier_handler_id); + + let app_state_clone_for_handler_flow = app_state.clone(); + let flow_handler_id = flow_scale_entry.connect_changed(move |entry| { + let mut state = app_state_clone_for_handler_flow.borrow_mut(); + if let Some(index) = state.selected_profile_index { + if let Some(profile) = state.config.game.get_mut(index) { + if let Ok(value) = entry.text().parse::() { + profile.flow_scale = round_to_2_decimals(value); + } + } + } + }); + app_state.borrow_mut().flow_scale_entry_handler_id = Some(flow_handler_id); + + let app_state_clone_for_handler_perf = app_state.clone(); + let perf_handler_id = performance_mode_switch.connect_state_set(move |_sw, active| { + let mut state = app_state_clone_for_handler_perf.borrow_mut(); + if let Some(index) = state.selected_profile_index { + if let Some(profile) = state.config.game.get_mut(index) { + profile.performance_mode = active; + } + } + drop(state); + glib::Propagation::Proceed + }); + app_state.borrow_mut().performance_mode_switch_handler_id = Some(perf_handler_id); + + let app_state_clone_for_handler_hdr = app_state.clone(); + let hdr_handler_id = hdr_mode_switch.connect_state_set(move |_sw, active| { + let mut state = app_state_clone_for_handler_hdr.borrow_mut(); + if let Some(index) = state.selected_profile_index { + if let Some(profile) = state.config.game.get_mut(index) { + profile.hdr_mode = active; + } + } + drop(state); + glib::Propagation::Proceed + }); + app_state.borrow_mut().hdr_mode_switch_handler_id = Some(hdr_handler_id); + + let app_state_clone_for_handler_exp = app_state.clone(); + let exp_handler_id = experimental_present_mode_dropdown.connect_selected_item_notify(move |dropdown| { + let mut state = app_state_clone_for_handler_exp.borrow_mut(); + if let Some(index) = state.selected_profile_index { + if let Some(profile) = state.config.game.get_mut(index) { + let selected_text = dropdown.selected_item().and_then(|item| item.downcast_ref::().map(|s| s.string().to_string())); + if let Some(text) = selected_text { + profile.experimental_present_mode = text; + } + } + } + }); + app_state.borrow_mut().experimental_present_mode_dropdown_handler_id = Some(exp_handler_id); + + let app_state_clone_save = app_state.clone(); + save_button.connect_clicked(move |_| { + let state_ref = app_state_clone_save.borrow(); + if let Some(index) = state_ref.selected_profile_index { + let multiplier_str = state_ref.multiplier_dropdown.selected_item().and_then(|item| item.downcast_ref::().map(|s| s.string().to_string())); + let flow_scale_text = state_ref.flow_scale_entry.text().to_string(); + let performance_mode_active = state_ref.performance_mode_switch.is_active(); + let hdr_mode_active = state_ref.hdr_mode_switch.is_active(); + let exp_mode_str = state_ref.experimental_present_mode_dropdown.selected_item().and_then(|item| item.downcast_ref::().map(|s| s.string().to_string())); + + drop(state_ref); + + let mut state = app_state_clone_save.borrow_mut(); + if let Some(profile) = state.config.game.get_mut(index) { + if let Some(text) = multiplier_str { + profile.multiplier = if text == "off" { 1 } else { text.parse().unwrap_or(1) }; + } + + if let Ok(value) = flow_scale_text.parse::() { + profile.flow_scale = round_to_2_decimals(value); + } + + profile.performance_mode = performance_mode_active; + profile.hdr_mode = hdr_mode_active; + + if let Some(text) = exp_mode_str { + profile.experimental_present_mode = text; + } + + state.save_current_config(); + + let feedback_label = Label::new(Some("Saved!")); + feedback_label.set_halign(gtk::Align::End); + feedback_label.set_margin_end(12); + feedback_label.set_margin_bottom(12); + + let main_settings_box_clone = state.main_settings_box.clone(); + + main_settings_box_clone.append(&feedback_label); + + glib::timeout_add_local(std::time::Duration::new(2, 0), move || { + main_settings_box_clone.remove(&feedback_label); + glib::ControlFlow::Break + }); + } + } + }); + + let app_state_clone_initial = app_state.clone(); + glib::idle_add_local(move || { + let mut state = app_state_clone_initial.borrow_mut(); + if state.config.game.first().is_some() { + state.selected_profile_index = Some(0); + } + state.populate_sidebar_with_handlers(Some(app_state_clone_initial.clone())); + drop(state); + + if app_state_clone_initial.borrow().selected_profile_index.is_some() { + app_state_clone_initial.borrow().update_main_window_from_profile(); + } + glib::ControlFlow::Break + }); + + main_window.present(); + }); + + application.run() +} diff --git a/ui/src/settings_window.rs b/ui/src/settings_window.rs new file mode 100644 index 0000000..73417e7 --- /dev/null +++ b/ui/src/settings_window.rs @@ -0,0 +1,155 @@ +use gtk::prelude::*; +use gtk::{glib, Label, Switch, Entry, Box, Orientation}; +use libadwaita::prelude::*; +use libadwaita::{ApplicationWindow, PreferencesGroup, PreferencesPage, PreferencesWindow, ActionRow}; +use std::rc::Rc; +use std::cell::RefCell; + +use crate::app_state::AppState; + +pub fn create_settings_window(parent: &ApplicationWindow, app_state: Rc>) -> PreferencesWindow { + let settings_window = PreferencesWindow::builder() + .title("Settings") + .transient_for(parent) + .modal(true) + .search_enabled(false) + .default_width(450) // Set default width + .default_height(300) // Set default height + .build(); + + let page = PreferencesPage::builder() + .title("General") + .icon_name("preferences-system-symbolic") + .build(); + + let group = PreferencesGroup::builder() + .title("Global Settings") + .build(); + + // --- Custom DLL Toggle and Path (Programmatically created) --- + let custom_dll_switch = Switch::builder() + .halign(gtk::Align::End) + .valign(gtk::Align::Center) + .build(); + + let custom_dll_path_entry = Entry::builder() + .placeholder_text("/path/to/Lossless.dll") + .hexpand(true) + .build(); + + let custom_dll_row = ActionRow::builder() + .title("Custom Path to Lossless.dll") + .build(); + custom_dll_row.add_suffix(&custom_dll_switch); + custom_dll_row.set_activatable_widget(Some(&custom_dll_switch)); + + let custom_dll_box = Box::builder() + .orientation(Orientation::Vertical) + .spacing(6) + .margin_top(6) + .margin_bottom(6) + .build(); + custom_dll_box.append(&custom_dll_row); + custom_dll_box.append(&custom_dll_path_entry); + + group.add(&custom_dll_box); // Add the box directly to the group + + // Initial state setup for Custom DLL + let current_dll_path = app_state.borrow().config.ordered_global.global.as_ref() + .and_then(|g| g.dll.clone()); + + if let Some(path) = current_dll_path { + custom_dll_switch.set_active(true); + custom_dll_path_entry.set_text(&path); + custom_dll_path_entry.set_visible(true); + } else { + custom_dll_switch.set_active(false); + custom_dll_path_entry.set_visible(false); + } + + // Connect switch to show/hide entry and update config + let app_state_clone_switch = app_state.clone(); + let custom_dll_path_entry_clone = custom_dll_path_entry.clone(); + custom_dll_switch.connect_state_set(move |_sw, active| { + custom_dll_path_entry_clone.set_visible(active); + let mut state = app_state_clone_switch.borrow_mut(); + if active { + // If activating, ensure global config exists and set DLL path + let current_path = custom_dll_path_entry_clone.text().to_string(); + state.config.ordered_global.global.get_or_insert_with(Default::default).dll = Some(current_path); + } else { + // If deactivating, set DLL path to None + if let Some(global_config) = state.config.ordered_global.global.as_mut() { + global_config.dll = None; + } + } + glib::Propagation::Proceed + }); + + // Connect entry to update config + let app_state_clone_entry = app_state.clone(); + let custom_dll_switch_clone = custom_dll_switch.clone(); + custom_dll_path_entry.connect_changed(move |entry| { + let mut state = app_state_clone_entry.borrow_mut(); + if custom_dll_switch_clone.is_active() { + let path = entry.text().to_string(); + if !path.is_empty() { + state.config.ordered_global.global.get_or_insert_with(Default::default).dll = Some(path); + } else { + // If path is cleared, set dll to None + if let Some(global_config) = state.config.ordered_global.global.as_mut() { + global_config.dll = None; + } + } + } + }); + + // Save button for settings + let save_settings_button = gtk::Button::builder() + .label("Save Global Settings") + .halign(gtk::Align::End) + .margin_end(12) + .margin_bottom(12) + .margin_top(12) + .build(); + + // Create a box to hold the feedback label + let feedback_container_box = Box::builder() + .orientation(Orientation::Vertical) + .halign(gtk::Align::End) + .margin_end(12) + .margin_bottom(12) + .build(); + + group.add(&save_settings_button); // Add button first + group.add(&feedback_container_box); // Then add the container for feedback + + let app_state_clone_save = app_state.clone(); + let feedback_container_box_clone = feedback_container_box.clone(); // Clone for the closure + save_settings_button.connect_clicked(move |_| { + let state = app_state_clone_save.borrow_mut(); // Removed 'mut' + state.save_current_config(); + + let feedback_label = Label::new(Some("Saved!")); + feedback_label.set_halign(gtk::Align::End); + feedback_label.set_margin_end(12); + feedback_label.set_margin_bottom(12); + + // Append to the dedicated feedback container box + feedback_container_box_clone.append(&feedback_label); + + glib::timeout_add_local(std::time::Duration::new(2, 0), { + let feedback_label_clone = feedback_label.clone(); // Clone for the timeout + let feedback_container_box_clone_for_remove = feedback_container_box_clone.clone(); // Clone for the timeout + move || { + feedback_container_box_clone_for_remove.remove(&feedback_label_clone); + glib::ControlFlow::Break + } + }); + }); + + page.add(&group); + settings_window.add(&page); + + settings_window +} diff --git a/ui/src/utils.rs b/ui/src/utils.rs new file mode 100644 index 0000000..7fe2612 --- /dev/null +++ b/ui/src/utils.rs @@ -0,0 +1,59 @@ +use std::process::Command; +use std::io::{BufReader, BufRead}; + +pub fn round_to_2_decimals(value: f32) -> f32 { + // Use string formatting to get exactly 2 decimal places and then parse back + // This avoids floating point precision issues + format!("{:.2}", value).parse().unwrap_or(value) +} + +/// Executes a bash command to find running processes that use Vulkan +/// and are owned by the current user. +/// Returns a vector of process names. +pub fn get_vulkan_processes() -> Vec { + let mut processes = Vec::new(); + let command_str = r#" + for pid in /proc/[0-9]*; do + owner=$(stat -c %U "$pid" 2>/dev/null) + if [[ "$owner" == "$USER" ]]; then + if grep -qi 'vulkan' "$pid/maps" 2>/dev/null; then + procname=$(cat "$pid/comm" 2>/dev/null) + if [[ -n "$procname" ]]; then + printf "%s\n" "$procname" # Only print the process name + fi + fi + fi + done + "#; + + // Execute the bash command + let output = Command::new("bash") + .arg("-c") + .arg(command_str) + .output(); + + match output { + Ok(output) => { + if output.status.success() { + // Read stdout line by line + let reader = BufReader::new(output.stdout.as_slice()); + for line in reader.lines() { + if let Ok(proc_name) = line { + let trimmed_name = proc_name.trim().to_string(); + if !trimmed_name.is_empty() { + processes.push(trimmed_name); + } + } + } + } else { + // Print stderr if the command failed + eprintln!("Command failed with error: {}", String::from_utf8_lossy(&output.stderr)); + } + }, + Err(e) => { + // Print error if the command could not be executed + eprintln!("Failed to execute command: {}", e); + } + } + processes +}