diff --git a/Cargo.lock b/Cargo.lock index e3c77f7d..088aabdf 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -4,9 +4,9 @@ version = 3 [[package]] name = "ab_glyph" -version = "0.2.26" +version = "0.2.27" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2e53b0a3d5760cd2ba9b787ae0c6440ad18ee294ff71b05e3381c900a7d16cfd" +checksum = "1c3a1cbc201cc13ed06cf875efb781f2249b3677f5c74571b67d817877f9d697" dependencies = [ "ab_glyph_rasterizer", "owned_ttf_parser", @@ -28,68 +28,92 @@ dependencies = [ "serde", ] +[[package]] +name = "accesskit" +version = "0.16.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e4700bdc115b306d6c43381c344dc307f03b7f0460c304e4892c309930322bd7" + +[[package]] +name = "accesskit_atspi_common" +version = "0.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a1de72dc7093910a1284cef784b6b143bab0a34d67f6178e4fc3aaaf29a09f8b" +dependencies = [ + "accesskit 0.16.0", + "accesskit_consumer", + "atspi-common", + "serde", + "thiserror", + "zvariant 3.15.2", +] + [[package]] name = "accesskit_consumer" -version = "0.16.1" +version = "0.24.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8c17cca53c09fbd7288667b22a201274b9becaa27f0b91bf52a526db95de45e6" +checksum = "fe3a07a32ab5837ad83db3230ac490c8504c2cd5b90ac8c00db6535f6ed65d0b" dependencies = [ - "accesskit", + "accesskit 0.16.0", + "immutable-chunkmap", ] [[package]] name = "accesskit_macos" -version = "0.10.1" +version = "0.17.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "cd3b6ae1eabbfbced10e840fd3fce8a93ae84f174b3e4ba892ab7bcb42e477a7" +checksum = "a189d159c153ae0fce5f9eefdcfec4a27885f453ce5ef0ccf078f72a73c39d34" dependencies = [ - "accesskit", + "accesskit 0.16.0", "accesskit_consumer", - "objc2 0.3.0-beta.3.patch-leaks.3", + "objc2", + "objc2-app-kit", + "objc2-foundation", "once_cell", ] [[package]] name = "accesskit_unix" -version = "0.6.2" +version = "0.12.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "09f46c18d99ba61ad7123dd13eeb0c104436ab6af1df6a1cd8c11054ed394a08" +checksum = "b76c448cfd96d16131a9ad3ab786d06951eb341cdac1db908978ab010245a19d" dependencies = [ - "accesskit", - "accesskit_consumer", + "accesskit 0.16.0", + "accesskit_atspi_common", "async-channel", - "async-once-cell", + "async-executor", + "async-task", "atspi", "futures-lite 1.13.0", - "once_cell", + "futures-util", "serde", "zbus 3.15.2", ] [[package]] name = "accesskit_windows" -version = "0.15.1" +version = "0.22.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "afcae27ec0974fc7c3b0b318783be89fd1b2e66dd702179fe600166a38ff4a0b" +checksum = "682d8c4fb425606f97408e7577793f32e96310b646fa77662eb4216293eddc7f" dependencies = [ - "accesskit", + "accesskit 0.16.0", "accesskit_consumer", - "once_cell", "paste", "static_assertions", - "windows 0.48.0", + "windows 0.54.0", ] [[package]] name = "accesskit_winit" -version = "0.16.1" +version = "0.22.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5284218aca17d9e150164428a0ebc7b955f70e3a9a78b4c20894513aabf98a67" +checksum = "9afbd6d598b7c035639ad2b664aa0edc94c93dc1fc3ebb4b40d8a95fcd43ffac" dependencies = [ - "accesskit", + "accesskit 0.16.0", "accesskit_macos", "accesskit_unix", "accesskit_windows", + "raw-window-handle", "winit", ] @@ -144,7 +168,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "37fe60779335388a88c01ac6c3be40304d1e349de3ada3b15f7808bb90fa9dce" dependencies = [ "alsa-sys", - "bitflags 2.5.0", + "bitflags 2.6.0", "libc", ] @@ -160,21 +184,21 @@ dependencies = [ [[package]] name = "android-activity" -version = "0.5.2" +version = "0.6.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ee91c0c2905bae44f84bfa4e044536541df26b7703fd0888deeb9060fcc44289" +checksum = "ef6978589202a00cd7e118380c448a08b6ed394c3a8df3a430d0898e3a42d046" dependencies = [ "android-properties", - "bitflags 2.5.0", + "bitflags 2.6.0", "cc", "cesu8", "jni", "jni-sys", "libc", "log", - "ndk", + "ndk 0.9.0", "ndk-context", - "ndk-sys", + "ndk-sys 0.6.0+11769913", "num_enum", "thiserror", ] @@ -235,10 +259,11 @@ checksum = "9fb4009533e8ff8f1450a5bcbc30f4242a1d34442221f72314bea1f5dc9c7f89" dependencies = [ "clipboard-win", "log", - "objc2 0.5.2", + "objc2", "objc2-app-kit", "objc2-foundation", "parking_lot", + "wl-clipboard-rs", "x11rb", ] @@ -284,7 +309,7 @@ dependencies = [ "serde", "serde_repr", "url", - "zbus 4.3.0", + "zbus 4.3.1", ] [[package]] @@ -427,12 +452,6 @@ dependencies = [ "futures-lite 2.3.0", ] -[[package]] -name = "async-once-cell" -version = "0.5.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9338790e78aa95a416786ec8389546c4b6a1dfc3dc36071ed9518a9413a542eb" - [[package]] name = "async-process" version = "1.8.1" @@ -478,7 +497,7 @@ checksum = "3b43422f69d8ff38f95f1b2bb76517c91589a924d1559a0e935d7c8ce0274c11" dependencies = [ "proc-macro2", "quote", - "syn 2.0.67", + "syn 2.0.68", ] [[package]] @@ -513,7 +532,7 @@ checksum = "c6fa2087f2753a7da8cc1c0dbfcf89579dd57458e36769de5ac750b4671737ca" dependencies = [ "proc-macro2", "quote", - "syn 2.0.67", + "syn 2.0.68", ] [[package]] @@ -618,7 +637,7 @@ version = "0.69.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "a00dc851838a2120612785d195287475a3ac45514741da670b735818822129a0" dependencies = [ - "bitflags 2.5.0", + "bitflags 2.6.0", "cexpr", "clang-sys", "itertools 0.12.1", @@ -629,7 +648,7 @@ dependencies = [ "regex", "rustc-hash", "shlex", - "syn 2.0.67", + "syn 2.0.68", ] [[package]] @@ -655,9 +674,9 @@ checksum = "bef38d45163c2f1dde094a7dfd33ccf595c92905c8f8f4fdc18d06fb1037718a" [[package]] name = "bitflags" -version = "2.5.0" +version = "2.6.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "cf4b9d6a944f767f8e5e0db018570623c85f3d925ac718db4e06d0187adb21c1" +checksum = "b048fb63fd8b5923fc5aa7b340d8e156aec7ec02f0c78fa8a6ddc2613f6f71de" dependencies = [ "serde", ] @@ -677,51 +696,13 @@ dependencies = [ "generic-array", ] -[[package]] -name = "block-sys" -version = "0.1.0-beta.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0fa55741ee90902547802152aaf3f8e5248aab7e21468089560d4c8840561146" -dependencies = [ - "objc-sys 0.2.0-beta.2", -] - -[[package]] -name = "block-sys" -version = "0.2.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ae85a0696e7ea3b835a453750bf002770776609115e6d25c6d2ff28a8200f7e7" -dependencies = [ - "objc-sys 0.3.5", -] - -[[package]] -name = "block2" -version = "0.2.0-alpha.6" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8dd9e63c1744f755c2f60332b88de39d341e5e86239014ad839bd71c106dec42" -dependencies = [ - "block-sys 0.1.0-beta.1", - "objc2-encode 2.0.0-pre.2", -] - -[[package]] -name = "block2" -version = "0.3.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "15b55663a85f33501257357e6421bb33e769d5c9ffb5ba0921c975a123e35e68" -dependencies = [ - "block-sys 0.2.1", - "objc2 0.4.1", -] - [[package]] name = "block2" version = "0.5.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "2c132eebf10f5cad5289222520a4a058514204aed6d791f1cf4fe8088b82d15f" dependencies = [ - "objc2 0.5.2", + "objc2", ] [[package]] @@ -760,7 +741,7 @@ checksum = "1ee891b04274a59bd38b412188e24b849617b2e45a0fd8d057deb63e7403761b" dependencies = [ "proc-macro2", "quote", - "syn 2.0.67", + "syn 2.0.68", ] [[package]] @@ -781,7 +762,7 @@ version = "0.12.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "fba7adb4dd5aa98e5553510223000e7148f621165ec5f9acd7113f6ca4995298" dependencies = [ - "bitflags 2.5.0", + "bitflags 2.6.0", "log", "polling 3.7.2", "rustix 0.38.34", @@ -809,9 +790,9 @@ checksum = "37b2a672a2cb129a2e41c10b1224bb368f9f37a2b16b612598138befd7b37eb5" [[package]] name = "cc" -version = "1.0.99" +version = "1.0.104" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "96c51067fd44124faa7f870b4b1c969379ad32b2ba805aa959430ceaa384f695" +checksum = "74b6a57f98764a267ff415d50a25e6e166f3831a5071af4995296ea97d210490" dependencies = [ "jobserver", "libc", @@ -900,14 +881,14 @@ checksum = "0b023947811758c97c59bf9d1c188fd619ad4718dcaa767947df1cadb14f39f4" dependencies = [ "glob", "libc", - "libloading 0.8.3", + "libloading 0.8.4", ] [[package]] name = "clap" -version = "4.5.7" +version = "4.5.8" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5db83dced34638ad474f39f250d7fea9598bdd239eaced1bdf45d597da0f433f" +checksum = "84b3edb18336f4df585bc9aa31dd99c036dfa5dc5e9a2939a722a188f3a8970d" dependencies = [ "clap_builder", "clap_derive", @@ -915,9 +896,9 @@ dependencies = [ [[package]] name = "clap_builder" -version = "4.5.7" +version = "4.5.8" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f7e204572485eb3fbf28f871612191521df159bc3e15a9f5064c66dba3a8c05f" +checksum = "c1c09dd5ada6c6c78075d6fd0da3f90d8080651e2d6cc8eb2f1aaa4034ced708" dependencies = [ "anstyle", "clap_lex", @@ -926,14 +907,14 @@ dependencies = [ [[package]] name = "clap_derive" -version = "4.5.5" +version = "4.5.8" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c780290ccf4fb26629baa7a1081e68ced113f1d3ec302fa5948f1c381ebf06c6" +checksum = "2bac35c6dafb060fd4d275d9a4ffae97917c13a6327903a8be2153cd964f7085" dependencies = [ "heck", "proc-macro2", "quote", - "syn 2.0.67", + "syn 2.0.68", ] [[package]] @@ -967,12 +948,6 @@ version = "0.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ecdffb913a326b6c642290a0d0ec8e8d6597291acdc07cc4c9cb4b3635d44cf9" -[[package]] -name = "color_quant" -version = "1.1.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3d7b894f5411737b7867f4827955924d7c254fc9f4d91a6aad6b097804b1018b" - [[package]] name = "com" version = "0.6.0" @@ -1107,7 +1082,7 @@ dependencies = [ "js-sys", "libc", "mach2", - "ndk", + "ndk 0.8.0", "ndk-context", "oboe", "wasm-bindgen", @@ -1253,12 +1228,12 @@ dependencies = [ [[package]] name = "d3d12" -version = "0.19.0" +version = "0.20.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3e3d747f100290a1ca24b752186f61f6637e1deffe3bf6320de6fcb29510a307" +checksum = "b28bfe653d79bd16c77f659305b195b82bb5ce0c0eb2a4846b82ddbd77586813" dependencies = [ - "bitflags 2.5.0", - "libloading 0.8.3", + "bitflags 2.6.0", + "libloading 0.8.4", "winapi", ] @@ -1288,6 +1263,17 @@ dependencies = [ "syn 1.0.109", ] +[[package]] +name = "derive-new" +version = "0.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d150dea618e920167e5973d70ae6ece4385b7164e0d799fe7c122dd0a5d912ad" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.68", +] + [[package]] name = "derive_arbitrary" version = "1.3.2" @@ -1296,7 +1282,7 @@ checksum = "67e77553c4162a157adbf834ebae5b415acbecbeafc7a74b0e886657506a7611" dependencies = [ "proc-macro2", "quote", - "syn 2.0.67", + "syn 2.0.68", ] [[package]] @@ -1344,7 +1330,7 @@ checksum = "97369cbbc041bc366949bc74d34658d6cda5621039731c6310521892a3a20ae0" dependencies = [ "proc-macro2", "quote", - "syn 2.0.67", + "syn 2.0.68", ] [[package]] @@ -1353,7 +1339,7 @@ version = "0.5.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "330c60081dcc4c72131f8eb70510f1ac07223e5d4163db481a04a0befcffa412" dependencies = [ - "libloading 0.8.3", + "libloading 0.8.4", ] [[package]] @@ -1371,13 +1357,30 @@ version = "1.2.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "75b325c5dbd37f80359721ad39aca5a29fb04c89279657cffdda8736d0c0b9d2" +[[package]] +name = "dpi" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f25c0e292a7ca6d6498557ff1df68f32c99850012b6ea401cf8daf771f22ff53" +dependencies = [ + "serde", +] + [[package]] name = "ecolor" version = "0.27.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "20930a432bbd57a6d55e07976089708d4893f3d556cf42a0d79e9e321fa73b10" + +[[package]] +name = "ecolor" +version = "0.28.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d2cabe0a45c3736c274bc650ca02ab293eb2ad1a3870f6033590ca7a3ee9963c" dependencies = [ "bytemuck", + "color-hex", + "emath 0.28.0", "serde", ] @@ -1387,57 +1390,36 @@ version = "0.27.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "584c5d1bf9a67b25778a3323af222dbe1a1feb532190e103901187f92c7fe29a" dependencies = [ - "accesskit", "ahash", - "epaint", - "log", + "epaint 0.27.2", "nohash-hasher", - "ron", - "serde", -] - -[[package]] -name = "egui-wgpu" -version = "0.27.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "469ff65843f88a702b731a1532b7d03b0e8e96d283e70f3a22b0e06c46cb9b37" -dependencies = [ - "bytemuck", - "document-features", - "egui", - "epaint", - "log", - "thiserror", - "type-map", - "web-time", - "wgpu", - "winit", ] [[package]] -name = "egui-winit" -version = "0.27.2" +name = "egui" +version = "0.28.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2e3da0cbe020f341450c599b35b92de4af7b00abde85624fd16f09c885573609" +checksum = "9dce16991290ee6395f3780b9b0db15bf0368bce2f31f2e9ba276d26e4b09cda" dependencies = [ - "accesskit_winit", - "arboard", - "egui", + "accesskit 0.12.3", + "ahash", + "emath 0.28.0", + "epaint 0.28.0", "log", - "raw-window-handle 0.6.2", - "smithay-clipboard", - "web-time", - "webbrowser 0.8.15", - "winit", + "nohash-hasher", + "puffin", + "ron", + "serde", ] [[package]] name = "egui_extras" -version = "0.27.2" +version = "0.28.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1b78779f35ded1a853786c9ce0b43fe1053e10a21ea3b23ebea411805ce41593" +checksum = "de9caa162e2d75084e637fbb46637fb864b7411e8ce772425a0e7e4746f81368" dependencies = [ - "egui", + "ahash", + "egui 0.28.0", "enum-map", "image", "log", @@ -1446,15 +1428,21 @@ dependencies = [ [[package]] name = "either" -version = "1.12.0" +version = "1.13.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3dca9240753cf90908d7e4aac30f630662b02aebaa1b58a3cadabdb23385b58b" +checksum = "60b1af1c220855b6ceac025d3f6ecdd2b7c4894bfe9cd9bda4fbb4bc7c0d4cf0" [[package]] name = "emath" version = "0.27.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e4c3a552cfca14630702449d35f41c84a0d15963273771c6059175a803620f3f" + +[[package]] +name = "emath" +version = "0.28.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "275f98c04af8359eeef56af16dfafd2bc7a65d9c0e5f2f00918057ca90a9fba8" dependencies = [ "bytemuck", "serde", @@ -1493,7 +1481,7 @@ checksum = "f282cfdfe92516eb26c2af8589c274c7c17681f5ecc03c18255fe741c6aa64eb" dependencies = [ "proc-macro2", "quote", - "syn 2.0.67", + "syn 2.0.68", ] [[package]] @@ -1505,7 +1493,7 @@ dependencies = [ "once_cell", "proc-macro2", "quote", - "syn 2.0.67", + "syn 2.0.68", ] [[package]] @@ -1526,7 +1514,7 @@ checksum = "de0d48a183585823424a4ce1aa132d174a6a81bd540895822eb4c8373a8e49e8" dependencies = [ "proc-macro2", "quote", - "syn 2.0.67", + "syn 2.0.68", ] [[package]] @@ -1537,7 +1525,7 @@ checksum = "6fd000fd6988e73bbe993ea3db9b1aa64906ab88766d654973924340c8cddb42" dependencies = [ "proc-macro2", "quote", - "syn 2.0.67", + "syn 2.0.68", ] [[package]] @@ -1545,15 +1533,30 @@ name = "epaint" version = "0.27.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b381f8b149657a4acf837095351839f32cd5c4aec1817fc4df84e18d76334176" +dependencies = [ + "ab_glyph", + "ahash", + "ecolor 0.27.2", + "emath 0.27.2", + "nohash-hasher", + "parking_lot", +] + +[[package]] +name = "epaint" +version = "0.28.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "205d6fcd02317e3e06cb32d28f821dd549b14ab12da6ff283dda57c4ebe4cb45" dependencies = [ "ab_glyph", "ahash", "bytemuck", - "ecolor", - "emath", + "ecolor 0.28.0", + "emath 0.28.0", "log", "nohash-hasher", "parking_lot", + "puffin", "serde", ] @@ -1641,6 +1644,12 @@ dependencies = [ "simd-adler32", ] +[[package]] +name = "fixedbitset" +version = "0.4.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0ce7134b9999ecaf8bcd65542e436736ef32ddca1b3e06094cb6ec5755203b80" + [[package]] name = "flate2" version = "1.0.30" @@ -1684,7 +1693,7 @@ checksum = "1a5c6c585bc94aaf2c7b51dd4c2ba22680844aba4c687be581871a6f518c5742" dependencies = [ "proc-macro2", "quote", - "syn 2.0.67", + "syn 2.0.68", ] [[package]] @@ -1766,7 +1775,7 @@ checksum = "87750cf4b7a4c0625b1529e4c543c2182106e4dedc60a2a6455e00d212c489ac" dependencies = [ "proc-macro2", "quote", - "syn 2.0.67", + "syn 2.0.68", ] [[package]] @@ -1917,7 +1926,7 @@ version = "0.6.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "fbcd2dba93594b227a1f57ee09b8b9da8892c34d55aa332e034a228d0fe6a171" dependencies = [ - "bitflags 2.5.0", + "bitflags 2.6.0", "gpu-alloc-types", ] @@ -1927,7 +1936,7 @@ version = "0.3.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "98ff03b468aa837d70984d55f5d3f846f6ec31fe34bbb97c4f85219caeee1ca4" dependencies = [ - "bitflags 2.5.0", + "bitflags 2.6.0", ] [[package]] @@ -1945,22 +1954,22 @@ dependencies = [ [[package]] name = "gpu-descriptor" -version = "0.2.4" +version = "0.3.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "cc11df1ace8e7e564511f53af41f3e42ddc95b56fd07b3f4445d2a6048bc682c" +checksum = "9c08c1f623a8d0b722b8b99f821eb0ba672a1618f0d3b16ddbee1cedd2dd8557" dependencies = [ - "bitflags 2.5.0", + "bitflags 2.6.0", "gpu-descriptor-types", "hashbrown", ] [[package]] name = "gpu-descriptor-types" -version = "0.1.2" +version = "0.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6bf0b36e6f090b7e1d8a4b49c0cb81c1f8376f72198c65dd3ad9ff3556b8b78c" +checksum = "fdf242682df893b86f33a73828fb09ca4b2d3bb6cc95249707fc684d27484b91" dependencies = [ - "bitflags 2.5.0", + "bitflags 2.6.0", ] [[package]] @@ -2008,10 +2017,10 @@ version = "0.11.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "af2a7e73e1f34c48da31fb668a907f250794837e08faa144fd24f0b8b741e890" dependencies = [ - "bitflags 2.5.0", + "bitflags 2.6.0", "com", "libc", - "libloading 0.8.3", + "libloading 0.8.4", "thiserror", "widestring", "winapi", @@ -2104,9 +2113,9 @@ checksum = "0fcc0b4a115bf80b728eb8ea024ad5bd707b615bfed49e0665b6e0f86fd082d9" [[package]] name = "hyper" -version = "1.3.1" +version = "1.4.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "fe575dd17d0862a9a33781c8c4696a55c320909004a67a00fb286ba8b1bc496d" +checksum = "c4fe55fb7a772d59a5ff1dfbff4fe0258d19b89fec4b233e75d35d5d2316badc" dependencies = [ "bytes", "futures-channel", @@ -2157,9 +2166,9 @@ dependencies = [ [[package]] name = "hyper-util" -version = "0.1.5" +version = "0.1.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7b875924a60b96e5d7b9ae7b066540b1dd1cbd90d1828f54c92e02a283351c56" +checksum = "3ab92f4f49ee4fb4f997c784b7a2e0fa70050211e0b6a287f898c3c9785ca956" dependencies = [ "bytes", "futures-channel", @@ -2198,17 +2207,6 @@ dependencies = [ "cc", ] -[[package]] -name = "icrate" -version = "0.0.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "99d3aaff8a54577104bafdf686ff18565c3b6903ca5782a2026ef06e2c7aa319" -dependencies = [ - "block2 0.3.0", - "dispatch", - "objc2 0.4.1", -] - [[package]] name = "idna" version = "0.5.0" @@ -2221,17 +2219,25 @@ dependencies = [ [[package]] name = "image" -version = "0.24.9" +version = "0.25.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5690139d2f55868e080017335e4b94cb7414274c74f1669c84fb5feba2c9f69d" +checksum = "fd54d660e773627692c524beaad361aca785a4f9f5730ce91f42aabe5bce3d11" dependencies = [ "bytemuck", "byteorder", - "color_quant", "num-traits", "png", ] +[[package]] +name = "immutable-chunkmap" +version = "2.0.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4419f022e55cc63d5bbd6b44b71e1d226b9c9480a47824c706e9d54e5c40c5eb" +dependencies = [ + "arrayvec", +] + [[package]] name = "indexmap" version = "2.2.6" @@ -2381,7 +2387,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "6aae1df220ece3c0ada96b8153459b67eebe9ae9212258bb0134ae60416fdf76" dependencies = [ "libc", - "libloading 0.8.3", + "libloading 0.8.4", "pkg-config", ] @@ -2421,9 +2427,9 @@ dependencies = [ [[package]] name = "libloading" -version = "0.8.3" +version = "0.8.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0c2a198fb6b0eada2a8df47933734e6d35d350665a33a3593d7164fa52c75c19" +checksum = "e310b3a6b5907f99202fcdb4960ff45b93735d7c7d96b760fcff8db2dc0e103d" dependencies = [ "cfg-if", "windows-targets 0.52.5", @@ -2435,7 +2441,7 @@ version = "0.0.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "3af92c55d7d839293953fcd0fda5ecfe93297cfde6ffbdec13b41d99c0ba6607" dependencies = [ - "bitflags 2.5.0", + "bitflags 2.6.0", "libc", "redox_syscall 0.4.1", ] @@ -2446,7 +2452,7 @@ version = "0.1.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "c0ff37bd590ca25063e35af745c343cb7a0271906fb7b37e4813e8f79f00268d" dependencies = [ - "bitflags 2.5.0", + "bitflags 2.6.0", "libc", ] @@ -2496,9 +2502,9 @@ checksum = "9374ef4228402d4b7e403e5838cb880d9ee663314b0a900d5a6aabf0c213552e" [[package]] name = "log" -version = "0.4.21" +version = "0.4.22" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "90ed8c1e510134f979dbc4f070f87d4313098b704861a105fe34231c70a3901c" +checksum = "a7a70ba024b9dc04c27ea2f0c0548feb474ec5c54bba33a7f72f873a39d07b24" [[package]] name = "lz4_flex" @@ -2559,11 +2565,11 @@ dependencies = [ [[package]] name = "metal" -version = "0.27.0" +version = "0.28.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c43f73953f8cbe511f021b58f18c3ce1c3d1ae13fe953293e13345bf83217f25" +checksum = "5637e166ea14be6063a3f8ba5ccb9a4159df7d8f6d61c02fc3d480b1f90dcfcb" dependencies = [ - "bitflags 2.5.0", + "bitflags 2.6.0", "block", "core-graphics-types", "foreign-types 0.5.0", @@ -2607,12 +2613,13 @@ dependencies = [ [[package]] name = "naga" -version = "0.19.2" +version = "0.20.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "50e3524642f53d9af419ab5e8dd29d3ba155708267667c2f3f06c88c9e130843" +checksum = "e536ae46fcab0876853bd4a632ede5df4b1c2527a58f6c5a4150fe86be858231" dependencies = [ + "arrayvec", "bit-set", - "bitflags 2.5.0", + "bitflags 2.6.0", "codespan-reporting", "hexf-parse", "indexmap", @@ -2654,12 +2661,26 @@ version = "0.8.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "2076a31b7010b17a38c01907c45b945e8f11495ee4dd588309718901b1f7a5b7" dependencies = [ - "bitflags 2.5.0", + "bitflags 2.6.0", "jni-sys", "log", - "ndk-sys", + "ndk-sys 0.5.0+25.2.9519653", "num_enum", - "raw-window-handle 0.6.2", + "thiserror", +] + +[[package]] +name = "ndk" +version = "0.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c3f42e7bbe13d351b6bead8286a43aac9534b82bd3cc43e47037f012ebfd62d4" +dependencies = [ + "bitflags 2.6.0", + "jni-sys", + "log", + "ndk-sys 0.6.0+11769913", + "num_enum", + "raw-window-handle", "thiserror", ] @@ -2678,6 +2699,15 @@ dependencies = [ "jni-sys", ] +[[package]] +name = "ndk-sys" +version = "0.6.0+11769913" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ee6cda3051665f1fb8d9e08fc35c96d5a244fb1be711a03b71118828afc9a873" +dependencies = [ + "jni-sys", +] + [[package]] name = "nix" version = "0.26.4" @@ -2696,11 +2726,10 @@ version = "0.28.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ab2156c4fce2f8df6c499cc1c763e4394b7482525bf2a9701c9d79d215f519e4" dependencies = [ - "bitflags 2.5.0", + "bitflags 2.6.0", "cfg-if", "cfg_aliases 0.1.1", "libc", - "memoffset 0.9.1", ] [[package]] @@ -2709,10 +2738,11 @@ version = "0.29.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "71e2746dc3a24dd78b3cfcb7be93368c6de9963d30f43a6a73998a9cf4b17b46" dependencies = [ - "bitflags 2.5.0", + "bitflags 2.6.0", "cfg-if", "cfg_aliases 0.2.1", "libc", + "memoffset 0.9.1", ] [[package]] @@ -2764,7 +2794,7 @@ checksum = "ed3955f1a9c7c0c15e092f9c887db08b1fc683305fdf6eb6684f22555355e202" dependencies = [ "proc-macro2", "quote", - "syn 2.0.67", + "syn 2.0.68", ] [[package]] @@ -2794,7 +2824,7 @@ dependencies = [ "proc-macro-crate 3.1.0", "proc-macro2", "quote", - "syn 2.0.67", + "syn 2.0.68", ] [[package]] @@ -2804,7 +2834,6 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "915b1b472bc21c53464d6c8461c9d3af805ba1ef837e1cac254428f4a77177b1" dependencies = [ "malloc_buf", - "objc_exception", ] [[package]] @@ -2818,12 +2847,6 @@ dependencies = [ "objc_id", ] -[[package]] -name = "objc-sys" -version = "0.2.0-beta.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "df3b9834c1e95694a05a828b59f55fa2afec6288359cda67146126b3f90a55d7" - [[package]] name = "objc-sys" version = "0.3.5" @@ -2832,49 +2855,52 @@ checksum = "cdb91bdd390c7ce1a8607f35f3ca7151b65afc0ff5ff3b34fa350f7d7c7e4310" [[package]] name = "objc2" -version = "0.3.0-beta.3.patch-leaks.3" +version = "0.5.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7e01640f9f2cb1220bbe80325e179e532cb3379ebcd1bf2279d703c19fe3a468" +checksum = "46a785d4eeff09c14c487497c162e92766fbb3e4059a71840cecc03d9a50b804" dependencies = [ - "block2 0.2.0-alpha.6", - "objc-sys 0.2.0-beta.2", - "objc2-encode 2.0.0-pre.2", + "objc-sys", + "objc2-encode", ] [[package]] -name = "objc2" -version = "0.4.1" +name = "objc2-app-kit" +version = "0.2.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "559c5a40fdd30eb5e344fbceacf7595a81e242529fb4e21cf5f43fb4f11ff98d" +checksum = "e4e89ad9e3d7d297152b17d39ed92cd50ca8063a89a9fa569046d41568891eff" dependencies = [ - "objc-sys 0.3.5", - "objc2-encode 3.0.0", + "bitflags 2.6.0", + "block2", + "libc", + "objc2", + "objc2-core-data", + "objc2-core-image", + "objc2-foundation", + "objc2-quartz-core", ] [[package]] -name = "objc2" -version = "0.5.2" +name = "objc2-cloud-kit" +version = "0.2.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "46a785d4eeff09c14c487497c162e92766fbb3e4059a71840cecc03d9a50b804" +checksum = "74dd3b56391c7a0596a295029734d3c1c5e7e510a4cb30245f8221ccea96b009" dependencies = [ - "objc-sys 0.3.5", - "objc2-encode 4.0.3", + "bitflags 2.6.0", + "block2", + "objc2", + "objc2-core-location", + "objc2-foundation", ] [[package]] -name = "objc2-app-kit" +name = "objc2-contacts" version = "0.2.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e4e89ad9e3d7d297152b17d39ed92cd50ca8063a89a9fa569046d41568891eff" +checksum = "a5ff520e9c33812fd374d8deecef01d4a840e7b41862d849513de77e44aa4889" dependencies = [ - "bitflags 2.5.0", - "block2 0.5.1", - "libc", - "objc2 0.5.2", - "objc2-core-data", - "objc2-core-image", + "block2", + "objc2", "objc2-foundation", - "objc2-quartz-core", ] [[package]] @@ -2883,9 +2909,9 @@ version = "0.2.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "617fbf49e071c178c0b24c080767db52958f716d9eabdf0890523aeae54773ef" dependencies = [ - "bitflags 2.5.0", - "block2 0.5.1", - "objc2 0.5.2", + "bitflags 2.6.0", + "block2", + "objc2", "objc2-foundation", ] @@ -2895,27 +2921,24 @@ version = "0.2.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "55260963a527c99f1819c4f8e3b47fe04f9650694ef348ffd2227e8196d34c80" dependencies = [ - "block2 0.5.1", - "objc2 0.5.2", + "block2", + "objc2", "objc2-foundation", "objc2-metal", ] [[package]] -name = "objc2-encode" -version = "2.0.0-pre.2" +name = "objc2-core-location" +version = "0.2.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "abfcac41015b00a120608fdaa6938c44cb983fee294351cc4bac7638b4e50512" +checksum = "000cfee34e683244f284252ee206a27953279d370e309649dc3ee317b37e5781" dependencies = [ - "objc-sys 0.2.0-beta.2", + "block2", + "objc2", + "objc2-contacts", + "objc2-foundation", ] -[[package]] -name = "objc2-encode" -version = "3.0.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d079845b37af429bfe5dfa76e6d087d788031045b25cfc6fd898486fd9847666" - [[package]] name = "objc2-encode" version = "4.0.3" @@ -2928,10 +2951,23 @@ version = "0.2.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "0ee638a5da3799329310ad4cfa62fbf045d5f56e3ef5ba4149e7452dcf89d5a8" dependencies = [ - "bitflags 2.5.0", - "block2 0.5.1", + "bitflags 2.6.0", + "block2", + "dispatch", "libc", - "objc2 0.5.2", + "objc2", +] + +[[package]] +name = "objc2-link-presentation" +version = "0.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a1a1ae721c5e35be65f01a03b6d2ac13a54cb4fa70d8a5da293d7b0020261398" +dependencies = [ + "block2", + "objc2", + "objc2-app-kit", + "objc2-foundation", ] [[package]] @@ -2940,9 +2976,9 @@ version = "0.2.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "dd0cba1276f6023976a406a14ffa85e1fdd19df6b0f737b063b95f6c8c7aadd6" dependencies = [ - "bitflags 2.5.0", - "block2 0.5.1", - "objc2 0.5.2", + "bitflags 2.6.0", + "block2", + "objc2", "objc2-foundation", ] @@ -2952,20 +2988,66 @@ version = "0.2.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e42bee7bff906b14b167da2bac5efe6b6a07e6f7c0a21a7308d40c960242dc7a" dependencies = [ - "bitflags 2.5.0", - "block2 0.5.1", - "objc2 0.5.2", + "bitflags 2.6.0", + "block2", + "objc2", "objc2-foundation", "objc2-metal", ] [[package]] -name = "objc_exception" -version = "0.1.2" +name = "objc2-symbols" +version = "0.2.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ad970fb455818ad6cba4c122ad012fae53ae8b4795f86378bce65e4f6bab2ca4" +checksum = "0a684efe3dec1b305badae1a28f6555f6ddd3bb2c2267896782858d5a78404dc" dependencies = [ - "cc", + "objc2", + "objc2-foundation", +] + +[[package]] +name = "objc2-ui-kit" +version = "0.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b8bb46798b20cd6b91cbd113524c490f1686f4c4e8f49502431415f3512e2b6f" +dependencies = [ + "bitflags 2.6.0", + "block2", + "objc2", + "objc2-cloud-kit", + "objc2-core-data", + "objc2-core-image", + "objc2-core-location", + "objc2-foundation", + "objc2-link-presentation", + "objc2-quartz-core", + "objc2-symbols", + "objc2-uniform-type-identifiers", + "objc2-user-notifications", +] + +[[package]] +name = "objc2-uniform-type-identifiers" +version = "0.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "44fa5f9748dbfe1ca6c0b79ad20725a11eca7c2218bceb4b005cb1be26273bfe" +dependencies = [ + "block2", + "objc2", + "objc2-foundation", +] + +[[package]] +name = "objc2-user-notifications" +version = "0.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "76cfcbf642358e8689af64cee815d139339f3ed8ad05103ed5eaf73db8d84cb3" +dependencies = [ + "bitflags 2.6.0", + "block2", + "objc2", + "objc2-core-location", + "objc2-foundation", ] [[package]] @@ -2979,9 +3061,9 @@ dependencies = [ [[package]] name = "object" -version = "0.36.0" +version = "0.36.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "576dfe1fc8f9df304abb159d767a29d0476f7750fbf8aa7ad07816004a207434" +checksum = "081b846d1d56ddfc18fdf1a922e4f6e07a11768ea1b92dec44e42b72712ccfce" dependencies = [ "memchr", ] @@ -2993,7 +3075,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e8b61bebd49e5d43f5f8cc7ee2891c16e0f41ec7954d36bcb6c14c5e0de867fb" dependencies = [ "jni", - "ndk", + "ndk 0.8.0", "ndk-context", "num-derive", "num-traits", @@ -3027,7 +3109,7 @@ version = "0.10.64" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "95a0481286a310808298130d22dd1fef0fa571e05a8f44ec801801e84b216b1f" dependencies = [ - "bitflags 2.5.0", + "bitflags 2.6.0", "cfg-if", "foreign-types 0.3.2", "libc", @@ -3044,7 +3126,7 @@ checksum = "a948666b637a0f465e8564c73e89d4dde00d72d4d473cc972f390fc3dcee7d9c" dependencies = [ "proc-macro2", "quote", - "syn 2.0.67", + "syn 2.0.68", ] [[package]] @@ -3090,6 +3172,16 @@ dependencies = [ "pin-project-lite", ] +[[package]] +name = "os_pipe" +version = "1.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "29d73ba8daf8fac13b0501d1abeddcfe21ba7401ada61a819144b6c2a4f32209" +dependencies = [ + "libc", + "windows-sys 0.52.0", +] + [[package]] name = "overload" version = "0.1.1" @@ -3146,6 +3238,16 @@ version = "2.3.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e3148f5046208a5d56bcfc03053e3ca6334e51da8dfb19b6cdc8b306fae3283e" +[[package]] +name = "petgraph" +version = "0.6.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b4c5cc86750666a3ed20bdaf5ca2a0344f9c67674cae0515bec2da16fbaa47db" +dependencies = [ + "fixedbitset", + "indexmap", +] + [[package]] name = "pin-project" version = "1.1.5" @@ -3163,7 +3265,7 @@ checksum = "2f38a4412a78282e09a2cf38d195ea5420d15ba0602cb375210efbc877243965" dependencies = [ "proc-macro2", "quote", - "syn 2.0.67", + "syn 2.0.68", ] [[package]] @@ -3340,7 +3442,7 @@ dependencies = [ "once_cell", "parking_lot", "serde", - "web-time", + "web-time 0.2.4", ] [[package]] @@ -3349,7 +3451,7 @@ version = "0.27.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "0bf67bdfe1838de5d81df11eb5d3bce78ae9cb7c2c8e172c3a3d768ae61a404a" dependencies = [ - "egui", + "egui 0.27.2", "indexmap", "natord", "once_cell", @@ -3357,7 +3459,7 @@ dependencies = [ "puffin", "time", "vec1", - "web-time", + "web-time 0.2.4", ] [[package]] @@ -3414,12 +3516,6 @@ version = "0.1.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9c8a99fddc9f0ba0a85884b8d14e3592853e787d581ca1816c91349b10e4eeab" -[[package]] -name = "raw-window-handle" -version = "0.5.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f2ff9a1f06a88b01621b7ae906ef0211290d1c8a168a15542486a8f61c0833b9" - [[package]] name = "raw-window-handle" version = "0.6.2" @@ -3446,15 +3542,6 @@ dependencies = [ "crossbeam-utils", ] -[[package]] -name = "redox_syscall" -version = "0.3.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "567664f262709473930a4bf9e51bf2ebf3348f2e748ccc50dea20646858f8f29" -dependencies = [ - "bitflags 1.3.2", -] - [[package]] name = "redox_syscall" version = "0.4.1" @@ -3470,7 +3557,7 @@ version = "0.5.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "c82cf8cff14456045f55ec4241383baeff27af886adb72ffb2162f99911de0fd" dependencies = [ - "bitflags 2.5.0", + "bitflags 2.6.0", ] [[package]] @@ -3578,7 +3665,7 @@ dependencies = [ "objc-foundation", "objc_id", "pollster", - "raw-window-handle 0.6.2", + "raw-window-handle", "urlencoding", "wasm-bindgen", "wasm-bindgen-futures", @@ -3617,7 +3704,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b91f7eff05f748767f183df4320a63d6936e9c6107d97c9e6bdd9784f4289c94" dependencies = [ "base64 0.21.7", - "bitflags 2.5.0", + "bitflags 2.6.0", "serde", "serde_derive", ] @@ -3654,7 +3741,7 @@ version = "0.38.34" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "70dc5ec042f7a43c4a73241207cecc9873a06d45debb38b329f8541d85c2730f" dependencies = [ - "bitflags 2.5.0", + "bitflags 2.6.0", "errno", "libc", "linux-raw-sys 0.4.14", @@ -3692,9 +3779,9 @@ checksum = "976295e77ce332211c0d24d92c0e83e50f5c5f046d11082cea19f3df13a3562d" [[package]] name = "rustls-webpki" -version = "0.102.4" +version = "0.102.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ff448f7e92e913c4b7d4c6d8e4540a1724b319b4152b8aef6d4cf8339712b33e" +checksum = "f9a6fccd794a42c2c105b513a2f62bc3fd8f3ba57a4593677ceb0bd035164d78" dependencies = [ "ring", "rustls-pki-types", @@ -3739,9 +3826,9 @@ checksum = "94143f37725109f92c262ed2cf5e59bce7498c01bcc1502d7b9afe439a4e9f49" [[package]] name = "sctk-adwaita" -version = "0.8.1" +version = "0.9.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "82b2eaf3a5b264a521b988b2e73042e742df700c4f962cde845d1541adb46550" +checksum = "7555fcb4f753d095d734fdefebb0ad8c98478a21db500492d87c55913d3b0086" dependencies = [ "ab_glyph", "log", @@ -3756,7 +3843,7 @@ version = "2.11.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "c627723fd09706bacdb5cf41499e95098555af3c3c29d014dc3c458ef6be11c0" dependencies = [ - "bitflags 2.5.0", + "bitflags 2.6.0", "core-foundation", "core-foundation-sys", "libc", @@ -3796,14 +3883,14 @@ checksum = "500cbc0ebeb6f46627f50f3f5811ccf6bf00643be300b4c3eabc0ef55dc5b5ba" dependencies = [ "proc-macro2", "quote", - "syn 2.0.67", + "syn 2.0.68", ] [[package]] name = "serde_json" -version = "1.0.117" +version = "1.0.120" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "455182ea6142b14f93f4bc5320a2b31c1f266b66a4a5c858b013302a5d8cbfc3" +checksum = "4e0d21c9a8cae1235ad58a00c11cb40d4b1e5c784f1ef2c537876ed6ffd8b7c5" dependencies = [ "itoa", "ryu", @@ -3818,7 +3905,7 @@ checksum = "6c64451ba24fc7a6a2d60fc75dd9c83c90903b19028d4eff35e88fc1e86564e9" dependencies = [ "proc-macro2", "quote", - "syn 2.0.67", + "syn 2.0.68", ] [[package]] @@ -3904,7 +3991,7 @@ version = "0.18.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "922fd3eeab3bd820d76537ce8f582b1cf951eceb5475c28500c7457d9d17f53a" dependencies = [ - "bitflags 2.5.0", + "bitflags 2.6.0", "calloop", "calloop-wayland-source", "cursor-icon", @@ -3923,17 +4010,6 @@ dependencies = [ "xkeysym", ] -[[package]] -name = "smithay-clipboard" -version = "0.7.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c091e7354ea8059d6ad99eace06dd13ddeedbb0ac72d40a9a6e7ff790525882d" -dependencies = [ - "libc", - "smithay-client-toolkit", - "wayland-backend", -] - [[package]] name = "smol_str" version = "0.2.2" @@ -3975,7 +4051,7 @@ version = "0.3.0+sdk-1.3.268.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "eda41003dc44290527a59b13432d4a0379379fa074b70174882adfbdfd917844" dependencies = [ - "bitflags 2.5.0", + "bitflags 2.6.0", ] [[package]] @@ -3998,9 +4074,9 @@ checksum = "7da8b5736845d9f2fcb837ea5d9e2628564b3b043a70948a3f0b778838c5fb4f" [[package]] name = "subtle" -version = "2.6.0" +version = "2.6.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0d0208408ba0c3df17ed26eb06992cb1a1268d41b2c0e12e65203fbe3972cee5" +checksum = "13c2bddecc57b384dee18652358fb23172facb8a2c51ccc10d74c157bdea3292" [[package]] name = "syn" @@ -4015,9 +4091,9 @@ dependencies = [ [[package]] name = "syn" -version = "2.0.67" +version = "2.0.68" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ff8655ed1d86f3af4ee3fd3263786bc14245ad17c4c7e85ba7187fb3ae028c90" +checksum = "901fa70d88b9d6c98022e23b4136f9f3e54e4662c3bc1bd1d84a42a9a0f0c1e9" dependencies = [ "proc-macro2", "quote", @@ -4041,7 +4117,6 @@ dependencies = [ "libc", "ntapi", "once_cell", - "rayon", "windows 0.52.0", ] @@ -4091,26 +4166,27 @@ dependencies = [ name = "tetanes" version = "0.11.0" dependencies = [ + "accesskit 0.16.0", + "accesskit_winit", "anyhow", + "arboard", "base64 0.22.1", "bincode", "bytemuck", "cfg-if", "chrono", "clap", - "color-hex", "console_error_panic_hook", "cpal", "crossbeam", "dirs", - "egui", - "egui-wgpu", - "egui-winit", + "egui 0.28.0", "egui_extras", "getrandom", "gilrs", "hound", "image", + "nohash-hasher", "parking_lot", "pollster", "puffin", @@ -4133,7 +4209,7 @@ dependencies = [ "wasm-bindgen", "wasm-bindgen-futures", "web-sys", - "webbrowser 1.0.1", + "webbrowser", "wgpu", "winit", "zip", @@ -4145,7 +4221,7 @@ version = "0.11.0" dependencies = [ "anyhow", "bincode", - "bitflags 2.5.0", + "bitflags 2.6.0", "cfg-if", "criterion", "dirs", @@ -4160,7 +4236,7 @@ dependencies = [ "tracing", "tracing-subscriber", "web-sys", - "web-time", + "web-time 1.1.0", ] [[package]] @@ -4199,7 +4275,7 @@ checksum = "46c3384250002a6d5af4d114f2845d37b57521033f30d5c3f46c4d70e1197533" dependencies = [ "proc-macro2", "quote", - "syn 2.0.67", + "syn 2.0.68", ] [[package]] @@ -4280,9 +4356,9 @@ dependencies = [ [[package]] name = "tinyvec" -version = "1.6.0" +version = "1.6.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "87cc5ceb3875bb20c2890005a4e226a4651264a5c75edb2421b52861a0a0cb50" +checksum = "c55115c6fbe2d2bef26eb09ad74bde02d8255476fc0c7b515ef09fbb35742d82" dependencies = [ "tinyvec_macros", ] @@ -4428,7 +4504,7 @@ checksum = "34704c8d6ebcbc939824180af020566b01a7c01f80641264eba0999f6c2b6be7" dependencies = [ "proc-macro2", "quote", - "syn 2.0.67", + "syn 2.0.68", ] [[package]] @@ -4479,6 +4555,20 @@ dependencies = [ "web-sys", ] +[[package]] +name = "tree_magic_mini" +version = "3.1.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "469a727cac55b41448315cc10427c069c618ac59bb6a4480283fcd811749bdc2" +dependencies = [ + "fnv", + "home", + "memchr", + "nom", + "once_cell", + "petgraph", +] + [[package]] name = "try-lock" version = "0.2.5" @@ -4491,15 +4581,6 @@ version = "0.21.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "2c591d83f69777866b9126b24c6dd9a18351f177e49d625920d19f989fd31cf8" -[[package]] -name = "type-map" -version = "0.5.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "deb68604048ff8fa93347f02441e4487594adc20bb8a084f9e564d2b827a0a9f" -dependencies = [ - "rustc-hash", -] - [[package]] name = "typenum" version = "1.17.0" @@ -4582,9 +4663,9 @@ checksum = "daf8dba3b7eb870caf1ddeed7bc9d2a049f3cfdfae7cb521b087cc33ae4c49da" [[package]] name = "uuid" -version = "1.8.0" +version = "1.9.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a183cf7feeba97b4dd1c0d46788634f6221d87fa961b305bed08c851829efcc0" +checksum = "5de17fd2f7da591098415cff336e12965a28061ddace43b59cb3c430179c9439" dependencies = [ "getrandom", "rand", @@ -4623,9 +4704,9 @@ checksum = "49874b5167b65d7193b8aba1567f5c7d93d001cafc34600cee003eda787e483f" [[package]] name = "waker-fn" -version = "1.1.1" +version = "1.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f3c4517f54858c779bbcbf228f4fca63d121bf85fbecb2dc578cdf4a39395690" +checksum = "317211a0dc0ceedd78fb2ca9a44aed3d7b9b26f81870d485c07122b4350673b7" [[package]] name = "walkdir" @@ -4673,7 +4754,7 @@ dependencies = [ "once_cell", "proc-macro2", "quote", - "syn 2.0.67", + "syn 2.0.68", "wasm-bindgen-shared", ] @@ -4707,7 +4788,7 @@ checksum = "e94f17b526d0a461a191c78ea52bbce64071ed5c04c9ffe424dcb38f74171bb7" dependencies = [ "proc-macro2", "quote", - "syn 2.0.67", + "syn 2.0.68", "wasm-bindgen-backend", "wasm-bindgen-shared", ] @@ -4738,7 +4819,7 @@ version = "0.31.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "1e63801c85358a431f986cffa74ba9599ff571fc5774ac113ed3b490c19a1133" dependencies = [ - "bitflags 2.5.0", + "bitflags 2.6.0", "rustix 0.38.34", "wayland-backend", "wayland-scanner", @@ -4750,7 +4831,7 @@ version = "0.3.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "625c5029dbd43d25e6aa9615e88b829a5cad13b2819c4ae129fdbb7c31ab4c7e" dependencies = [ - "bitflags 2.5.0", + "bitflags 2.6.0", "cursor-icon", "wayland-backend", ] @@ -4772,7 +4853,7 @@ version = "0.31.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "8f81f365b8b4a97f422ac0e8737c438024b5951734506b0e1d775c73030561f4" dependencies = [ - "bitflags 2.5.0", + "bitflags 2.6.0", "wayland-backend", "wayland-client", "wayland-scanner", @@ -4784,7 +4865,7 @@ version = "0.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "23803551115ff9ea9bce586860c5c5a971e360825a0309264102a9495a5ff479" dependencies = [ - "bitflags 2.5.0", + "bitflags 2.6.0", "wayland-backend", "wayland-client", "wayland-protocols", @@ -4797,7 +4878,7 @@ version = "0.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ad1f61b76b6c2d8742e10f9ba5c3737f6530b4c243132c2a2ccc8aa96fe25cd6" dependencies = [ - "bitflags 2.5.0", + "bitflags 2.6.0", "wayland-backend", "wayland-client", "wayland-protocols", @@ -4848,20 +4929,13 @@ dependencies = [ ] [[package]] -name = "webbrowser" -version = "0.8.15" +name = "web-time" +version = "1.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "db67ae75a9405634f5882791678772c94ff5f16a66535aae186e26aa0841fc8b" +checksum = "5a6580f308b1fad9207618087a65c04e7a10bc77e02c8e84e9b00dd4b12fa0bb" dependencies = [ - "core-foundation", - "home", - "jni", - "log", - "ndk-context", - "objc", - "raw-window-handle 0.5.2", - "url", - "web-sys", + "js-sys", + "wasm-bindgen", ] [[package]] @@ -4870,13 +4944,13 @@ version = "1.0.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "425ba64c1e13b1c6e8c5d2541c8fac10022ca584f33da781db01b5756aef1f4e" dependencies = [ - "block2 0.5.1", + "block2", "core-foundation", "home", "jni", "log", "ndk-context", - "objc2 0.5.2", + "objc2", "objc2-foundation", "url", "web-sys", @@ -4884,19 +4958,20 @@ dependencies = [ [[package]] name = "wgpu" -version = "0.19.4" +version = "0.20.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "cbd7311dbd2abcfebaabf1841a2824ed7c8be443a0f29166e5d3c6a53a762c01" +checksum = "90e37c7b9921b75dfd26dd973fdcbce36f13dfa6e2dc82aece584e0ed48c355c" dependencies = [ "arrayvec", "cfg-if", "cfg_aliases 0.1.1", + "document-features", "js-sys", "log", "naga", "parking_lot", "profiling", - "raw-window-handle 0.6.2", + "raw-window-handle", "smallvec", "static_assertions", "wasm-bindgen", @@ -4909,22 +4984,23 @@ dependencies = [ [[package]] name = "wgpu-core" -version = "0.19.4" +version = "0.21.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "28b94525fc99ba9e5c9a9e24764f2bc29bad0911a7446c12f446a8277369bf3a" +checksum = "d50819ab545b867d8a454d1d756b90cd5f15da1f2943334ca314af10583c9d39" dependencies = [ "arrayvec", "bit-vec", - "bitflags 2.5.0", + "bitflags 2.6.0", "cfg_aliases 0.1.1", "codespan-reporting", + "document-features", "indexmap", "log", "naga", "once_cell", "parking_lot", "profiling", - "raw-window-handle 0.6.2", + "raw-window-handle", "rustc-hash", "smallvec", "thiserror", @@ -4935,15 +5011,15 @@ dependencies = [ [[package]] name = "wgpu-hal" -version = "0.19.4" +version = "0.21.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "fc1a4924366df7ab41a5d8546d6534f1f33231aa5b3f72b9930e300f254e39c3" +checksum = "172e490a87295564f3fcc0f165798d87386f6231b04d4548bca458cbbfd63222" dependencies = [ "android_system_properties", "arrayvec", "ash", "bit-set", - "bitflags 2.5.0", + "bitflags 2.6.0", "block", "cfg_aliases 0.1.1", "core-graphics-types", @@ -4957,17 +5033,17 @@ dependencies = [ "js-sys", "khronos-egl", "libc", - "libloading 0.8.3", + "libloading 0.8.4", "log", "metal", "naga", - "ndk-sys", + "ndk-sys 0.5.0+25.2.9519653", "objc", "once_cell", "parking_lot", "profiling", "range-alloc", - "raw-window-handle 0.6.2", + "raw-window-handle", "renderdoc-sys", "rustc-hash", "smallvec", @@ -4980,11 +5056,11 @@ dependencies = [ [[package]] name = "wgpu-types" -version = "0.19.2" +version = "0.20.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b671ff9fb03f78b46ff176494ee1ebe7d603393f42664be55b64dc8d53969805" +checksum = "1353d9a46bff7f955a680577f34c69122628cc2076e1d6f3a9be6ef00ae793ef" dependencies = [ - "bitflags 2.5.0", + "bitflags 2.6.0", "js-sys", "web-sys", ] @@ -5026,17 +5102,6 @@ version = "0.4.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "712e227841d057c1ee1cd2fb22fa7e5a5461ae8e48fa2ca79ec42cfc1931183f" -[[package]] -name = "windows" -version = "0.48.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e686886bc078bc1b0b600cac0147aadb815089b6e4da64016cbd754b6342700f" -dependencies = [ - "windows-implement 0.48.0", - "windows-interface 0.48.0", - "windows-targets 0.48.5", -] - [[package]] name = "windows" version = "0.52.0" @@ -5054,6 +5119,8 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9252e5725dbed82865af151df558e754e4a3c2c30818359eb17465f1346a1b49" dependencies = [ "windows-core 0.54.0", + "windows-implement 0.53.0", + "windows-interface 0.53.0", "windows-targets 0.52.5", ] @@ -5100,13 +5167,13 @@ dependencies = [ [[package]] name = "windows-implement" -version = "0.48.0" +version = "0.53.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5e2ee588991b9e7e6c8338edf3333fbe4da35dc72092643958ebb43f0ab2c49c" +checksum = "942ac266be9249c84ca862f0a164a39533dc2f6f33dc98ec89c8da99b82ea0bd" dependencies = [ "proc-macro2", "quote", - "syn 1.0.109", + "syn 2.0.68", ] [[package]] @@ -5117,18 +5184,18 @@ checksum = "9107ddc059d5b6fbfbffdfa7a7fe3e22a226def0b2608f72e9d552763d3e1ad7" dependencies = [ "proc-macro2", "quote", - "syn 2.0.67", + "syn 2.0.68", ] [[package]] name = "windows-interface" -version = "0.48.0" +version = "0.53.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e6fb8df20c9bcaa8ad6ab513f7b40104840c8867d5751126e4df3b08388d0cc7" +checksum = "da33557140a288fae4e1d5f8873aaf9eb6613a9cf82c3e070223ff177f598b60" dependencies = [ "proc-macro2", "quote", - "syn 1.0.109", + "syn 2.0.68", ] [[package]] @@ -5139,7 +5206,7 @@ checksum = "29bee4b38ea3cde66011baa44dba677c432a78593e202392d1e9070cf2a7fca7" dependencies = [ "proc-macro2", "quote", - "syn 2.0.67", + "syn 2.0.68", ] [[package]] @@ -5358,38 +5425,42 @@ checksum = "bec47e5bfd1bff0eeaf6d8b485cc1074891a197ab4225d504cb7a1ab88b02bf0" [[package]] name = "winit" -version = "0.29.15" +version = "0.30.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0d59ad965a635657faf09c8f062badd885748428933dad8e8bdd64064d92e5ca" +checksum = "49f45a7b7e2de6af35448d7718dab6d95acec466eb3bb7a56f4d31d1af754004" dependencies = [ "ahash", "android-activity", "atomic-waker", - "bitflags 2.5.0", + "bitflags 2.6.0", + "block2", "bytemuck", "calloop", - "cfg_aliases 0.1.1", + "cfg_aliases 0.2.1", + "concurrent-queue", "core-foundation", "core-graphics", "cursor-icon", - "icrate", + "dpi", "js-sys", "libc", - "log", "memmap2", - "ndk", - "ndk-sys", - "objc2 0.4.1", - "once_cell", + "ndk 0.9.0", + "objc2", + "objc2-app-kit", + "objc2-foundation", + "objc2-ui-kit", "orbclient", "percent-encoding", - "raw-window-handle 0.6.2", - "redox_syscall 0.3.5", + "pin-project", + "raw-window-handle", + "redox_syscall 0.4.1", "rustix 0.38.34", "sctk-adwaita", "serde", "smithay-client-toolkit", "smol_str", + "tracing", "unicode-segmentation", "wasm-bindgen", "wasm-bindgen-futures", @@ -5398,8 +5469,8 @@ dependencies = [ "wayland-protocols", "wayland-protocols-plasma", "web-sys", - "web-time", - "windows-sys 0.48.0", + "web-time 1.1.0", + "windows-sys 0.52.0", "x11-dl", "x11rb", "xkbcommon-dl", @@ -5424,6 +5495,26 @@ dependencies = [ "windows-sys 0.48.0", ] +[[package]] +name = "wl-clipboard-rs" +version = "0.8.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "12b41773911497b18ca8553c3daaf8ec9fe9819caf93d451d3055f69de028adb" +dependencies = [ + "derive-new", + "libc", + "log", + "nix 0.28.0", + "os_pipe", + "tempfile", + "thiserror", + "tree_magic_mini", + "wayland-backend", + "wayland-client", + "wayland-protocols", + "wayland-protocols-wlr", +] + [[package]] name = "x11-dl" version = "2.21.0" @@ -5444,7 +5535,7 @@ dependencies = [ "as-raw-xcb-connection", "gethostname", "libc", - "libloading 0.8.3", + "libloading 0.8.4", "once_cell", "rustix 0.38.34", "x11rb-protocol", @@ -5478,7 +5569,7 @@ version = "0.4.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d039de8032a9a8856a6be89cea3e5d12fdd82306ab7c94d74e6deab2460651c5" dependencies = [ - "bitflags 2.5.0", + "bitflags 2.6.0", "dlib", "log", "once_cell", @@ -5540,9 +5631,9 @@ dependencies = [ [[package]] name = "zbus" -version = "4.3.0" +version = "4.3.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "23915fcb26e7a9a9dc05fd93a9870d336d6d032cd7e8cebf1c5c37666489fdd5" +checksum = "851238c133804e0aa888edf4a0229481c753544ca12a60fd1c3230c8a500fe40" dependencies = [ "async-broadcast 0.7.1", "async-executor", @@ -5560,7 +5651,7 @@ dependencies = [ "futures-sink", "futures-util", "hex", - "nix 0.28.0", + "nix 0.29.0", "ordered-stream", "rand", "serde", @@ -5571,9 +5662,9 @@ dependencies = [ "uds_windows", "windows-sys 0.52.0", "xdg-home", - "zbus_macros 4.3.0", + "zbus_macros 4.3.1", "zbus_names 3.0.0", - "zvariant 4.1.1", + "zvariant 4.1.2", ] [[package]] @@ -5592,14 +5683,14 @@ dependencies = [ [[package]] name = "zbus_macros" -version = "4.3.0" +version = "4.3.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "02bcca0b586d2f8589da32347b4784ba424c4891ed86aa5b50d5e88f6b2c4f5d" +checksum = "8d5a3f12c20bd473be3194af6b49d50d7bb804ef3192dc70eddedb26b85d9da7" dependencies = [ "proc-macro-crate 3.1.0", "proc-macro2", "quote", - "syn 2.0.67", + "syn 2.0.68", "zvariant_utils 2.0.0", ] @@ -5622,27 +5713,27 @@ checksum = "4b9b1fef7d021261cc16cba64c351d291b715febe0fa10dc3a443ac5a5022e6c" dependencies = [ "serde", "static_assertions", - "zvariant 4.1.1", + "zvariant 4.1.2", ] [[package]] name = "zerocopy" -version = "0.7.34" +version = "0.7.35" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ae87e3fcd617500e5d106f0380cf7b77f3c6092aae37191433159dda23cfb087" +checksum = "1b9b4fd18abc82b8136838da5d50bae7bdea537c574d8dc1a34ed098d6c166f0" dependencies = [ "zerocopy-derive", ] [[package]] name = "zerocopy-derive" -version = "0.7.34" +version = "0.7.35" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "15e934569e47891f7d9411f1a451d947a60e000ab3bd24fbb970f000387d1b3b" +checksum = "fa4f8080344d4671fb4e831a13ad1e68092748387dfc4f55e356242fae12ce3e" dependencies = [ "proc-macro2", "quote", - "syn 2.0.67", + "syn 2.0.68", ] [[package]] @@ -5698,16 +5789,16 @@ dependencies = [ [[package]] name = "zvariant" -version = "4.1.1" +version = "4.1.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9aa6d31a02fbfb602bfde791de7fedeb9c2c18115b3d00f3a36e489f46ffbbc7" +checksum = "1724a2b330760dc7d2a8402d841119dc869ef120b139d29862d6980e9c75bfc9" dependencies = [ "endi", "enumflags2", "serde", "static_assertions", "url", - "zvariant_derive 4.1.1", + "zvariant_derive 4.1.2", ] [[package]] @@ -5725,14 +5816,14 @@ dependencies = [ [[package]] name = "zvariant_derive" -version = "4.1.1" +version = "4.1.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "642bf1b6b6d527988b3e8193d20969d53700a36eac734d21ae6639db168701c8" +checksum = "55025a7a518ad14518fb243559c058a2e5b848b015e31f1d90414f36e3317859" dependencies = [ "proc-macro-crate 3.1.0", "proc-macro2", "quote", - "syn 2.0.67", + "syn 2.0.68", "zvariant_utils 2.0.0", ] @@ -5755,5 +5846,5 @@ checksum = "fc242db087efc22bd9ade7aa7809e4ba828132edc312871584a6b4391bdf8786" dependencies = [ "proc-macro2", "quote", - "syn 2.0.67", + "syn 2.0.68", ] diff --git a/Cargo.toml b/Cargo.toml index cb1cc360..b5192f2e 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -41,7 +41,7 @@ clap = { version = "4.5", default-features = false, features = [ "derive", ] } dirs = "5.0" -image = { version = "0.24", default-features = false, features = ["png"] } +image = { version = "0.25", default-features = false, features = ["png"] } puffin = "0.19" serde = { version = "1.0", features = ["derive"] } tetanes-core = { version = "0.11", path = "tetanes-core" } @@ -52,7 +52,7 @@ tracing = { version = "0.1", default-features = false, features = [ ] } tracing-subscriber = "0.3" serde_json = "1.0" -web-time = "0.2" # FIXME: winit is using an old version +web-time = "1.0" web-sys = "0.3" # Playable framerates in development @@ -76,7 +76,7 @@ inherits = "release" strip = true [profile.dev.package."*"] -opt-level = 3 +opt-level = 2 [workspace.metadata.cross.target.x86_64-unknown-linux-gnu] pre-build = [ diff --git a/tetanes/Cargo.toml b/tetanes/Cargo.toml index 1a72b048..52e1a4e4 100644 --- a/tetanes/Cargo.toml +++ b/tetanes/Cargo.toml @@ -30,7 +30,12 @@ workspace = true [features] default = ["tetanes-core/cycle-accurate"] -profiling = ["tetanes-core/profiling", "dep:puffin", "dep:puffin_egui"] +profiling = [ + "tetanes-core/profiling", + "dep:puffin", + "dep:puffin_egui", + "egui/puffin", +] cycle-accurate = [] [dependencies] @@ -38,19 +43,28 @@ anyhow.workspace = true bincode.workspace = true bytemuck = "1.15" cfg-if.workspace = true +chrono = { version = "0.4", default-features = false, features = ["clock"] } +cpal = { version = "0.15", features = ["wasm-bindgen"] } crossbeam = "0.8" -# TODO: Remove once https://github.com/emilk/egui/pull/4372 is released -color-hex = "0.2" dirs.workspace = true -egui-wgpu = { version = "0.27", features = ["winit", "wayland", "x11"] } -egui_extras = { version = "0.27", default-features = false, features = [ +egui = { version = "0.28", features = [ + "bytemuck", + "color-hex", + "default_fonts", + "log", + "persistence", + "serde", +] } +egui_extras = { version = "0.28", default-features = false, features = [ "image", + "serde", ] } gilrs = { version = "0.10", features = ["serde-serialize"] } hound = "3.5" image.workspace = true +nohash-hasher = "0.2" parking_lot = "0.12" -puffin = { workspace = true, optional = true } +puffin = { workspace = true, optional = true, features = ["web"] } puffin_egui = { version = "0.27", optional = true } ringbuf = "0.4" serde.workspace = true @@ -61,49 +75,32 @@ thiserror.workspace = true tracing.workspace = true tracing-subscriber.workspace = true uuid = { version = "1.8", features = ["v4", "fast-rng", "serde"] } -# Only here to define features for egui-winit "links" feature, hence the "*" -webbrowser = { version = "*", features = ["hardened", "disable-wsl"] } -winit = { version = "0.29", features = ["serde"] } +webbrowser = { version = "1.0", features = ["hardened", "disable-wsl"] } +wgpu = { version = "0.20", features = ["webgl", "webgpu"] } +winit = { version = "0.30", features = ["serde"] } [target.'cfg(not(target_arch = "wasm32"))'.dependencies] -clap.workspace = true -cpal = "0.15" -chrono = { version = "0.4", default-features = false, features = ["clock"] } -egui = { version = "0.27", features = [ - "extra_debug_asserts", - "log", - "persistence", - "accesskit", +accesskit = "0.16" +accesskit_winit = "0.22" +arboard = { version = "3.4", default-features = false, features = [ + "wayland-data-control", ] } -egui-winit = { version = "0.27", features = ["accesskit"] } +clap.workspace = true +egui = { version = "0.28", default-features = false, features = ["accesskit"] } pollster = "0.3" reqwest = { version = "0.12", features = ["blocking"] } rfd = "0.14" semver = "1" -sysinfo = "0.30" +sysinfo = { version = "0.30", default-features = false } tracing-appender = "0.2" -wgpu = "0.19" [target.'cfg(target_arch = "wasm32")'.dependencies] -chrono = { version = "0.4", default-features = false, features = [ - "clock", - "wasmbind", -] } +base64 = "0.22" +chrono = { version = "0.4", default-features = false, features = ["wasmbind"] } console_error_panic_hook = "0.1" -cpal = { version = "0.15", features = ["wasm-bindgen"] } -egui = { version = "0.27", features = [ - "extra_debug_asserts", - "log", - "persistence", -] } -egui-winit = { version = "0.27", default-features = false, features = [ - "links", -] } # Required because of downstream dependencies: https://docs.rs/getrandom/latest/getrandom/#webassembly-support getrandom = { version = "0.2", features = ["js"] } -puffin = { workspace = true, features = ["web"], optional = true } tracing-web = "0.1" -wgpu = { version = "0.19", features = ["webgl", "webgpu"] } web-sys = { workspace = true, features = [ "Clipboard", "ClipboardEvent", @@ -127,7 +124,6 @@ web-sys = { workspace = true, features = [ wasm-bindgen = "0.2" wasm-bindgen-futures = "0.4" zip = { version = "2.1", default-features = false, features = ["deflate"] } -base64 = "0.22" [package.metadata.docs.rs] rustc-args = ["--cfg=web_sys_unstable_apis"] diff --git a/tetanes/shaders/crt-easymode.wgsl b/tetanes/shaders/crt-easymode.wgsl index a9001c90..aa97d2ee 100644 --- a/tetanes/shaders/crt-easymode.wgsl +++ b/tetanes/shaders/crt-easymode.wgsl @@ -37,8 +37,8 @@ var vertices: array, 3> = array, 3>( struct VertexOutput { @builtin(position) position: vec4, - @location(0) dims: vec2, - @location(1) inv_dims: vec2, + @location(0) tex_dims: vec2, + @location(1) inv_tex_dims: vec2, @location(2) v_uv: vec2, }; @@ -50,23 +50,25 @@ fn vs_main( let vert = vertices[v_idx]; // Convert x from -1.0..1.0 to 0.0..1.0 and y from -1.0..1.0 to 1.0..0.0 - out.dims = vec2(textureDimensions(tex)); - out.inv_dims = 1.0 / out.dims; - out.v_uv = fma(vert, vec2(0.5, -0.5), vec2(0.5, 0.5)); out.position = vec4(vert, 0.0, 1.0); + out.tex_dims = vec2(textureDimensions(tex)); + out.inv_tex_dims = 1.0 / out.tex_dims; + out.v_uv = fma(vert, vec2(0.5, -0.5), vec2(0.5, 0.5)); return out; } // Fragment shader struct Output { - size: vec2, - padding: vec2, + screen_size: vec2, + // Uniform buffers need to be at least 16 bytes in WebGL. + // See https://github.com/gfx-rs/wgpu/issues/2072 + _padding: vec2, } - @group(0) @binding(0) var out: Output; -@group(0) @binding(1) var tex: texture_2d; -@group(0) @binding(2) var tex_sampler: sampler; + +@group(1) @binding(0) var tex: texture_2d; +@group(1) @binding(1) var tex_sampler: sampler; const PI = 3.141592653589; @@ -77,15 +79,15 @@ const MASK_DOT_WIDTH = 1.0; const MASK_DOT_HEIGHT = 1.0; const MASK_STAGGER = 0.0; const MASK_SIZE = 1.0; -const SCANLINE_STRENGTH = 0.95; -const SCANLINE_BEAM_WIDTH_MIN = 2.5; -const SCANLINE_BEAM_WIDTH_MAX = 2.5; -const SCANLINE_BRIGHT_MIN = 0.3; -const SCANLINE_BRIGHT_MAX = 0.6; +const SCANLINE_STRENGTH = 1.0; +const SCANLINE_BEAM_WIDTH_MIN = 1.5; +const SCANLINE_BEAM_WIDTH_MAX = 1.5; +const SCANLINE_BRIGHT_MIN = 0.35; +const SCANLINE_BRIGHT_MAX = 0.65; const SCANLINE_CUTOFF = 400.0; -const GAMMA_INPUT = 1.0; -const GAMMA_OUTPUT = 2.2; -const BRIGHT_BOOST = 1.1; +const GAMMA_INPUT = 2.0; +const GAMMA_OUTPUT = 1.8; +const BRIGHT_BOOST = 1.2; const DILATION = 1.0; // apply half-circle s-curve to distance for sharper (more pixelated) interpolation @@ -123,12 +125,12 @@ fn get_color_matrix(co: vec2, dx: vec2) -> mat4x4 { @fragment fn fs_main( - @location(0) dims: vec2, - @location(1) inv_dims: vec2, + @location(0) tex_dims: vec2, + @location(1) inv_tex_dims: vec2, @location(2) v_uv: vec2 ) -> @location(0) vec4 { - let pix_co = v_uv * dims - vec2(0.5, 0.5); - let tex_co = (floor(pix_co) + vec2(0.5, 0.5)) * inv_dims; + let pix_co = v_uv * tex_dims - vec2(0.5, 0.5); + let tex_co = (floor(pix_co) + vec2(0.5, 0.5)) * inv_tex_dims; let dist = fract(pix_co); var curve_x = curve_distance(dist.x, SHARPNESS_H * SHARPNESS_H); @@ -138,8 +140,8 @@ fn fs_main( coeffs = 2.0 * sin(coeffs) * sin(coeffs * 0.5) / (coeffs * coeffs); coeffs /= dot(coeffs, vec4(1.0)); - let dx = vec2(inv_dims.x, 0.0); - let dy = vec2(0.0, inv_dims.y); + let dx = vec2(inv_tex_dims.x, 0.0); + let dy = vec2(0.0, inv_tex_dims.y); var col = filter_lanczos(coeffs, get_color_matrix(tex_co, dx)); var col2 = filter_lanczos(coeffs, get_color_matrix(tex_co + dy, dx)); @@ -150,11 +152,11 @@ fn fs_main( let bright = (max(col.r, max(col.g, col.b)) + luma) * 0.5; let scan_bright = clamp(bright, SCANLINE_BRIGHT_MIN, SCANLINE_BRIGHT_MAX); let scan_beam = clamp(bright * SCANLINE_BEAM_WIDTH_MAX, SCANLINE_BEAM_WIDTH_MIN, SCANLINE_BEAM_WIDTH_MAX); - var scan_weight = 1.0 - pow(cos(v_uv.y * 2.0 * PI * dims.y) * 0.5 + 0.5, scan_beam) * SCANLINE_STRENGTH; + var scan_weight = 1.0 - pow(cos(v_uv.y * 2.0 * PI * tex_dims.y) * 0.5 + 0.5, scan_beam) * SCANLINE_STRENGTH; - let insize = dims; + let insize = tex_dims; let mask = 1.0 - MASK_STRENGTH; - let mod_fac = floor(v_uv * out.size * dims / (insize * vec2(MASK_SIZE, MASK_DOT_HEIGHT * MASK_SIZE))); + let mod_fac = floor(v_uv * out.screen_size * tex_dims / (insize * vec2(MASK_SIZE, MASK_DOT_HEIGHT * MASK_SIZE))); let dot_no = i32(((mod_fac.x + (mod_fac.y % 2.0) * MASK_STAGGER) / MASK_DOT_WIDTH % 3.0)); var mask_weight: vec3; diff --git a/tetanes/shaders/gui.wgsl b/tetanes/shaders/gui.wgsl new file mode 100644 index 00000000..f28c7527 --- /dev/null +++ b/tetanes/shaders/gui.wgsl @@ -0,0 +1,83 @@ +// Vertex shader + +struct VertexOutput { + @builtin(position) position: vec4, + @location(0) v_uv: vec2, + @location(1) v_color: vec4, // gamma 0-1 +}; + +struct Output { + screen_size: vec2, + // Uniform buffers need to be at least 16 bytes in WebGL. + // See https://github.com/gfx-rs/wgpu/issues/2072 + _padding: vec2, +}; +@group(0) @binding(0) var out: Output; + +// 0-1 linear from 0-1 sRGB gamma +fn linear_from_gamma_rgb(srgb: vec3) -> vec3 { + let cutoff = srgb < vec3(0.04045); + let lower = srgb / vec3(12.92); + let higher = pow((srgb + vec3(0.055)) / vec3(1.055), vec3(2.4)); + return select(higher, lower, cutoff); +} + +// 0-1 sRGB gamma from 0-1 linear +fn gamma_from_linear_rgb(rgb: vec3) -> vec3 { + let cutoff = rgb < vec3(0.0031308); + let lower = rgb * vec3(12.92); + let higher = vec3(1.055) * pow(rgb, vec3(1.0 / 2.4)) - vec3(0.055); + return select(higher, lower, cutoff); +} + +// 0-1 sRGBA gamma from 0-1 linear +fn gamma_from_linear_rgba(linear_rgba: vec4) -> vec4 { + return vec4(gamma_from_linear_rgb(linear_rgba.rgb), linear_rgba.a); +} + +// [u8; 4] SRGB as u32 -> [r, g, b, a] in 0.-1 +fn unpack_color(color: u32) -> vec4 { + return vec4( + f32(color & 255u), + f32((color >> 8u) & 255u), + f32((color >> 16u) & 255u), + f32((color >> 24u) & 255u), + ) / 255.0; +} + +fn position_from_screen(screen_pos: vec2) -> vec4 { + return vec4( + 2.0 * screen_pos.x / out.screen_size.x - 1.0, + 1.0 - 2.0 * screen_pos.y / out.screen_size.y, + 0.0, + 1.0, + ); +} + +@vertex +fn vs_main( + @location(0) v_pos: vec2, + @location(1) v_uv: vec2, + @location(2) v_color: u32, +) -> VertexOutput { + var out: VertexOutput; + out.v_uv = v_uv; + out.v_color = unpack_color(v_color); + out.position = position_from_screen(v_pos); + return out; +} + +// Fragment shader + +@group(1) @binding(0) var tex: texture_2d; +@group(1) @binding(1) var tex_sampler: sampler; + +@fragment +fn fs_main( + @location(0) v_uv: vec2, + @location(1) v_color: vec4 +) -> @location(0) vec4 { + let tex = textureSample(tex, tex_sampler, v_uv); + let tex_gamma = gamma_from_linear_rgba(tex); + return v_color * tex_gamma; +} diff --git a/tetanes/src/nes.rs b/tetanes/src/nes.rs index 6a89ab34..dd84cd66 100644 --- a/tetanes/src/nes.rs +++ b/tetanes/src/nes.rs @@ -2,28 +2,26 @@ use crate::{ nes::{ - event::{NesEventProxy, RendererEvent, RunState, UiEvent}, + event::{NesEventProxy, RunState}, input::{Gamepads, InputBindings}, - renderer::{FrameRecycle, Resources}, + renderer::{painter::Painter, FrameRecycle, Resources}, }, - platform::{EventLoopExt, Initialize}, - thread, + platform::Initialize, }; use anyhow::Context; +use cfg_if::cfg_if; use config::Config; -use crossbeam::channel::{self, Receiver}; -use egui::{ahash::HashMap, ViewportBuilder}; -use egui_wgpu::winit::Painter; +use crossbeam::channel::Receiver; +use egui::ahash::HashMap; use emulation::Emulation; use event::NesEvent; use renderer::Renderer; use std::sync::Arc; use tetanes_core::{time::Instant, video::Frame}; use thingbuf::mpsc::blocking; -use tracing::{debug, error}; use winit::{ event::Modifiers, - event_loop::{EventLoop, EventLoopBuilder, EventLoopWindowTarget}, + event_loop::{ActiveEventLoop, EventLoop}, window::{Window, WindowId}, }; @@ -58,7 +56,6 @@ pub(crate) enum State { Pending { ctx: egui::Context, window: Arc, - viewport_builder: ViewportBuilder, painter_rx: Receiver, }, Running(Running), @@ -97,10 +94,17 @@ impl Nes { /// If event loop fails to build or run, then an error is returned. pub fn run(cfg: Config) -> anyhow::Result<()> { // Set up window, events and NES state - let event_loop = EventLoopBuilder::::with_user_event().build()?; - let mut nes = Nes::new(cfg, &event_loop); - event_loop - .run_platform(move |event, window_target| nes.event_loop(event, window_target))?; + let event_loop = EventLoop::::with_user_event().build()?; + let nes = Nes::new(cfg, &event_loop); + cfg_if! { + if #[cfg(target_arch = "wasm32")] { + use winit::platform::web::EventLoopExtWebSys; + event_loop.spawn_app(nes); + } else { + let mut nes = nes; + event_loop.run_app(&mut nes)?; + } + } Ok(()) } @@ -118,41 +122,20 @@ impl Nes { /// /// Returns an error if any resources can't be created correctly or `init_running` has already /// been called. - pub(crate) fn request_resources( + pub(crate) fn request_renderer_resources( &mut self, - event_loop: &EventLoopWindowTarget, + event_loop: &ActiveEventLoop, ) -> anyhow::Result<()> { let (cfg, tx) = self .init_state .as_ref() .context("config unexpectedly already taken")?; - let ctx = egui::Context::default(); - let (window, viewport_builder) = Renderer::create_window(event_loop, &ctx, cfg)?; - let window = Arc::new(window); - - let (painter_tx, painter_rx) = channel::bounded(1); - thread::spawn({ - let window = Arc::clone(&window); - let event_tx = tx.clone(); - async move { - debug!("creating painter..."); - match Renderer::create_painter(window).await { - Ok(painter) => { - painter_tx.send(painter).expect("failed to send painter"); - event_tx.event(RendererEvent::ResourcesReady); - } - Err(err) => { - error!("failed to create painter: {err:?}"); - event_tx.event(UiEvent::Terminate); - } - } - } - }); + + let (ctx, window, painter_rx) = Renderer::request_resources(event_loop, tx, cfg)?; self.state = State::Pending { ctx, window, - viewport_builder, painter_rx, }; @@ -166,21 +149,16 @@ impl Nes { /// /// If GPU resources failed to be requested, the emulation or renderer fails to build, then an /// error is returned. - pub(crate) fn init_running( - &mut self, - event_loop: &EventLoopWindowTarget, - ) -> anyhow::Result<()> { + pub(crate) fn init_running(&mut self) -> anyhow::Result<()> { match std::mem::take(&mut self.state) { State::Pending { ctx, window, - viewport_builder, painter_rx, } => { let resources = Resources { ctx, window, - viewport_builder, painter: painter_rx.recv()?, }; let (frame_tx, frame_rx) = blocking::with_recycle::(10, FrameRecycle); @@ -194,7 +172,7 @@ impl Nes { cfg.input.update_gamepad_assignments(&gamepads); let emulation = Emulation::new(tx.clone(), frame_tx.clone(), &cfg)?; - let renderer = Renderer::new(tx.clone(), event_loop, resources, frame_rx, &cfg)?; + let renderer = Renderer::new(tx.clone(), resources, frame_rx, &cfg)?; let mut running = Running { cfg, diff --git a/tetanes/src/nes/config.rs b/tetanes/src/nes/config.rs index b657b7e7..4af3ec9d 100644 --- a/tetanes/src/nes/config.rs +++ b/tetanes/src/nes/config.rs @@ -191,14 +191,21 @@ impl InputConfig { pub fn clear_binding(&mut self, input: Input) { for bind in &mut self.action_bindings { - if let Some(existing_input) = bind.bindings.iter_mut().find(|i| **i == Some(input)) { - if let Action::Deck(DeckAction::Joypad((player, _))) = bind.action { - self.joypads[player as usize].remove(&bind.action); + if let Some((binding, existing_input)) = bind + .bindings + .iter_mut() + .enumerate() + .find(|(_, i)| **i == Some(input)) + { + let keybinds = if let Action::Deck(DeckAction::Joypad((player, _))) = bind.action { + &mut self.joypads[player as usize] } else { - self.shortcuts.remove(&bind.action); - } + &mut self.shortcuts + }; + keybinds + .entry(bind.action) + .and_modify(|bind| bind.bindings[binding] = None); *existing_input = None; - break; } } } diff --git a/tetanes/src/nes/emulation.rs b/tetanes/src/nes/emulation.rs index 6d07a149..8db5a354 100644 --- a/tetanes/src/nes/emulation.rs +++ b/tetanes/src/nes/emulation.rs @@ -453,6 +453,7 @@ impl State { } } } + EmulationEvent::RequestFrame => self.send_frame(), EmulationEvent::Rewinding(rewind) => { if self.control_deck.is_running() { if self.rewind.enabled { @@ -891,17 +892,6 @@ impl State { } if let Some(park_timeout) = self.park_duration() { - self.tx.event(RendererEvent::RequestRedraw { - viewport_id: ViewportId::ROOT, - when: Instant::now() + park_timeout, - }); - if self.control_deck.is_running() && self.run_state.paused() { - // Only send a frame if there's space, which means the renderer consumed previous - // frames and may need the current buffer to redraw for e.g. resizing - if let Ok(mut frame) = self.frame_tx.try_send_ref() { - self.control_deck.frame_buffer_into(&mut frame); - } - } thread::park_timeout(park_timeout); return; } diff --git a/tetanes/src/nes/event.rs b/tetanes/src/nes/event.rs index ae5dc624..a319f3fa 100644 --- a/tetanes/src/nes/event.rs +++ b/tetanes/src/nes/event.rs @@ -29,11 +29,12 @@ use tetanes_core::{ time::{Duration, Instant}, video::VideoFilter, }; -use tracing::{error, trace}; +use tracing::{debug, error, trace}; use uuid::Uuid; use winit::{ - event::{ElementState, Event, WindowEvent}, - event_loop::{ControlFlow, DeviceEvents, EventLoop, EventLoopProxy, EventLoopWindowTarget}, + application::ApplicationHandler, + event::{DeviceEvent, DeviceId, ElementState, WindowEvent}, + event_loop::{ActiveEventLoop, ControlFlow, DeviceEvents, EventLoop, EventLoopProxy}, keyboard::PhysicalKey, window::WindowId, }; @@ -60,6 +61,13 @@ impl NesEventProxy { } } +#[derive(Default, Debug, Copy, Clone)] +#[must_use] +pub struct Response { + pub consumed: bool, + pub repaint: bool, +} + #[derive(Debug, Clone, PartialEq)] #[must_use] pub enum UiEvent { @@ -173,6 +181,7 @@ pub enum EmulationEvent { RunState(RunState), ReplayRecord(bool), Reset(ResetKind), + RequestFrame, Rewinding(bool), SaveState(u8), ShowFrameStats(bool), @@ -202,6 +211,14 @@ pub enum RendererEvent { Menu(Menu), } +#[cfg(not(target_arch = "wasm32"))] +#[derive(Debug, Clone)] +pub enum AccessKitWindowEvent { + InitialTreeRequested, + ActionRequested(accesskit::ActionRequest), + AccessibilityDeactivated, +} + #[derive(Debug, Clone)] #[must_use] pub enum NesEvent { @@ -209,11 +226,11 @@ pub enum NesEvent { Emulation(EmulationEvent), Renderer(RendererEvent), Config(ConfigEvent), - // For some reason ActionRequestEvent isn't Clone + // For some reason accesskit_winit::Event isn't Clone #[cfg(not(target_arch = "wasm32"))] AccessKit { window_id: WindowId, - request: egui::accesskit::ActionRequest, + event: AccessKitWindowEvent, }, } @@ -242,405 +259,492 @@ impl From for NesEvent { } #[cfg(not(target_arch = "wasm32"))] -impl From for NesEvent { - fn from(event: egui_winit::accesskit_winit::ActionRequestEvent) -> Self { +impl From for NesEvent { + fn from(event: accesskit_winit::Event) -> Self { + use accesskit_winit::WindowEvent; Self::AccessKit { window_id: event.window_id, - request: event.request, + event: match event.window_event { + WindowEvent::InitialTreeRequested => AccessKitWindowEvent::InitialTreeRequested, + WindowEvent::ActionRequested(request) => { + AccessKitWindowEvent::ActionRequested(request) + } + WindowEvent::AccessibilityDeactivated => { + AccessKitWindowEvent::AccessibilityDeactivated + } + }, } } } -impl Nes { - pub fn event_loop( - &mut self, - event: Event, - event_loop: &EventLoopWindowTarget, - ) { +impl ApplicationHandler for Nes { + fn user_event(&mut self, event_loop: &ActiveEventLoop, event: NesEvent) { #[cfg(feature = "profiling")] puffin::profile_function!(); - if !matches!(event, Event::NewEvents(..) | Event::AboutToWait) { - trace!("event: {event:?}"); - } + trace!("user event: {event:?}"); - match &event { - Event::Resumed => { - let state = if let State::Running(state) = &mut self.state { - if feature!(Suspend) { - state.renderer.recreate_window(event_loop); - } - state - } else { - if self.state.is_suspended() { - if let Err(err) = self.request_resources(event_loop) { - error!("failed to request renderer resources: {err:?}"); - event_loop.exit(); - } - } + match event { + NesEvent::Renderer(RendererEvent::ResourcesReady) => { + if let Err(err) = self.init_running() { + error!("failed to create window: {err:?}"); + event_loop.exit(); return; - }; - if let Some(window_id) = state.renderer.root_window_id() { - state.repaint_times.insert(window_id, Instant::now()); } - } - Event::UserEvent(event) => match event { - NesEvent::Renderer(RendererEvent::ResourcesReady) => { - if let Err(err) = self.init_running(event_loop) { - error!("failed to create window: {err:?}"); - event_loop.exit(); - return; - } - // Disable device events to save some cpu as they're mostly duplicated in - // WindowEvents - event_loop.listen_device_events(DeviceEvents::Never); - - if let State::Running(state) = &mut self.state { - if let Some(window) = state.renderer.root_window() { - if window.is_visible().unwrap_or(true) { - state.repaint_times.insert(window.id(), Instant::now()); - } else { - // Immediately redraw the root window on start if not - // visible. Fixes a bug where `window.request_redraw()` events - // may not be sent if the window isn't visible, which is the - // case until the first frame is drawn. - if let Err(err) = state.renderer.redraw( - window.id(), - event_loop, - &mut state.gamepads, - &mut state.cfg, - ) { - state.renderer.on_error(err); - } + // Disable device events to save some cpu as they're mostly duplicated in + // WindowEvents + event_loop.listen_device_events(DeviceEvents::Never); + + if let State::Running(state) = &mut self.state { + if let Some(window) = state.renderer.root_window() { + if window.is_visible().unwrap_or(true) { + state.repaint_times.insert(window.id(), Instant::now()); + } else { + // Immediately redraw the root window on start if not + // visible. Fixes a bug where `window.request_redraw()` events + // may not be sent if the window isn't visible, which is the + // case until the first frame is drawn. + if let Err(err) = state.renderer.redraw( + window.id(), + event_loop, + &mut state.gamepads, + &mut state.cfg, + ) { + state.renderer.on_error(err); } } } } - NesEvent::Ui(UiEvent::Terminate) => event_loop.exit(), - _ => (), - }, - Event::LoopExiting => { - #[cfg(feature = "profiling")] - puffin::set_scopes_on(false); - - if feature!(AbortOnExit) && !matches!(self.state, State::Running(_)) { - panic!("exited unexpectedly"); - } } + NesEvent::Ui(UiEvent::Terminate) => event_loop.exit(), _ => (), } if let State::Running(state) = &mut self.state { - state.on_event(event, event_loop); + state.user_event(event_loop, event); + } + } - let mut next_repaint_time = state.repaint_times.values().min().copied(); - state.repaint_times.retain(|window_id, when| { - if *when > Instant::now() { - return true; - } - next_repaint_time = None; + fn resumed(&mut self, event_loop: &ActiveEventLoop) { + #[cfg(feature = "profiling")] + puffin::profile_function!(); - if let Some(window) = state.renderer.window(*window_id) { - if !window.is_minimized().unwrap_or(false) { - window.request_redraw(); - } - // Repaint time will get removed as soon as we receive the RequestRedraw event - true - } else { - false + debug!("resumed event"); + + let state = if let State::Running(state) = &mut self.state { + if feature!(Suspend) { + state.renderer.recreate_window(event_loop); + } + state + } else { + if self.state.is_suspended() { + if let Err(err) = self.request_renderer_resources(event_loop) { + error!("failed to request renderer resources: {err:?}"); + event_loop.exit(); } - }); + } + return; + }; + if let Some(window_id) = state.renderer.root_window_id() { + state.repaint_times.insert(window_id, Instant::now()); + } + } - event_loop.set_control_flow(ControlFlow::WaitUntil(match next_repaint_time { - Some(next_repaint_time) => next_repaint_time, - None => Instant::now() + Duration::from_millis(16), - })); + fn window_event( + &mut self, + event_loop: &ActiveEventLoop, + window_id: WindowId, + event: WindowEvent, + ) { + #[cfg(feature = "profiling")] + puffin::profile_function!(); + + trace!("window event: {window_id:?} {event:?}"); + + if let State::Running(state) = &mut self.state { + state.window_event(event_loop, window_id, event); } } -} -impl Running { - pub fn on_event( + fn device_event( &mut self, - event: Event, - event_loop: &EventLoopWindowTarget, + event_loop: &ActiveEventLoop, + device_id: DeviceId, + event: DeviceEvent, ) { #[cfg(feature = "profiling")] puffin::profile_function!(); + trace!("device event: {device_id:?} {event:?}"); + + if let State::Running(state) = &mut self.state { + state.device_event(event_loop, device_id, event); + } + } + + fn about_to_wait(&mut self, event_loop: &ActiveEventLoop) { + #[cfg(feature = "profiling")] + puffin::profile_function!(); + + if let State::Running(state) = &mut self.state { + state.about_to_wait(event_loop); + } + } + + fn suspended(&mut self, event_loop: &ActiveEventLoop) { + #[cfg(feature = "profiling")] + puffin::profile_function!(); + + debug!("suspended event"); + + if let State::Running(state) = &mut self.state { + state.suspended(event_loop); + } + } + + fn exiting(&mut self, event_loop: &ActiveEventLoop) { + debug!("exiting"); + + #[cfg(feature = "profiling")] + puffin::set_scopes_on(false); + + if let State::Running(state) = &mut self.state { + state.exiting(event_loop); + } else if feature!(AbortOnExit) { + panic!("exited unexpectedly"); + } + } +} + +impl ApplicationHandler for Running { + fn user_event(&mut self, _event_loop: &ActiveEventLoop, event: NesEvent) { match event { - Event::Suspended => { - if feature!(Suspend) { - if let Err(err) = self.renderer.drop_window() { - error!("failed to suspend window: {err:?}"); - event_loop.exit(); + NesEvent::Config(ref event) => { + let Config { + deck, + emulation, + audio, + renderer, + input, + } = &mut self.cfg; + match event { + ConfigEvent::ActionBindings(bindings) => { + input.action_bindings.clone_from(bindings); + self.input_bindings = InputBindings::from_input_config(input); } - } - } - Event::MemoryWarning => { - self.renderer - .add_message(MessageType::Warn, "Your system memory is running low..."); - if self.cfg.emulation.rewind { - self.cfg.emulation.rewind = false; - self.event(ConfigEvent::RewindEnabled(false)); - } - } - Event::AboutToWait => { - self.gamepads.update_events(); - if let Some(window_id) = self.renderer.root_window_id() { - let res = self.renderer.on_gamepad_update(&self.gamepads); - if res.repaint { - self.repaint_times.insert(window_id, Instant::now()); + ConfigEvent::ActionBindingSet((action, set_input, binding)) => { + input.set_binding(*action, *set_input, *binding); + self.input_bindings.insert(*set_input, *action); } - - if res.consumed { - self.gamepads.clear_events(); - } else { - while let Some(event) = self.gamepads.next_event() { - self.on_gamepad_event(window_id, event); - self.repaint_times.insert(window_id, Instant::now()); + ConfigEvent::ActionBindingClear(clear_input) => { + input.clear_binding(*clear_input); + self.input_bindings.remove(clear_input); + } + ConfigEvent::AlwaysOnTop(always_on_top) => { + renderer.always_on_top = *always_on_top; + self.renderer + .set_always_on_top(self.cfg.renderer.always_on_top); + } + ConfigEvent::ApuChannelEnabled((channel, enabled)) => { + deck.channels_enabled[*channel as usize] = *enabled; + } + ConfigEvent::ApuChannelsEnabled(enabled) => { + deck.channels_enabled = *enabled; + } + ConfigEvent::AudioBuffer(buffer_size) => { + audio.buffer_size = *buffer_size; + } + ConfigEvent::AudioEnabled(enabled) => audio.enabled = *enabled, + ConfigEvent::AudioLatency(latency) => audio.latency = *latency, + ConfigEvent::AutoLoad(enabled) => emulation.auto_load = *enabled, + ConfigEvent::AutoSave(enabled) => emulation.auto_save = *enabled, + ConfigEvent::AutoSaveInterval(interval) => { + emulation.auto_save_interval = *interval; + } + ConfigEvent::ConcurrentDpad(enabled) => deck.concurrent_dpad = *enabled, + ConfigEvent::CycleAccurate(enabled) => deck.cycle_accurate = *enabled, + ConfigEvent::DarkTheme(enabled) => renderer.dark_theme = *enabled, + ConfigEvent::EmbedViewports(embed) => renderer.embed_viewports = *embed, + ConfigEvent::FourPlayer(four_player) => deck.four_player = *four_player, + ConfigEvent::Fullscreen(fullscreen) => renderer.fullscreen = *fullscreen, + ConfigEvent::GamepadAssign((player, uuid)) => { + input.assign_gamepad(*player, *uuid); + if let Some(name) = self.gamepads.gamepad_name_by_uuid(uuid) { + self.tx.event(UiEvent::Message(( + MessageType::Info, + format!("Assigned gamepad `{name}` to player {player:?}.",), + ))); + } + } + ConfigEvent::GamepadUnassign(player) => { + if let Some(uuid) = input.unassign_gamepad(*player) { + if let Some(name) = self.gamepads.gamepad_name_by_uuid(&uuid) { + self.tx.event(UiEvent::Message(( + MessageType::Info, + format!("Unassigned gamepad `{name}` from player {player:?}."), + ))); + } } } + ConfigEvent::GamepadAssignments(assignments) => { + input.gamepad_assignments = *assignments; + } + ConfigEvent::GenieCodeAdded(genie_code) => { + deck.genie_codes.push(genie_code.clone()); + } + ConfigEvent::GenieCodeClear => deck.genie_codes.clear(), + ConfigEvent::GenieCodeRemoved(code) => { + deck.genie_codes.retain(|genie| genie.code() != code); + } + ConfigEvent::HideOverscan(hide) => renderer.hide_overscan = *hide, + ConfigEvent::MapperRevisions(revs) => deck.mapper_revisions = *revs, + ConfigEvent::RamState(ram_state) => deck.ram_state = *ram_state, + ConfigEvent::RecentRomsClear => renderer.recent_roms.clear(), + ConfigEvent::Region(region) => deck.region = *region, + ConfigEvent::RewindEnabled(enabled) => emulation.rewind = *enabled, + ConfigEvent::RewindInterval(interval) => { + emulation.rewind_interval = *interval; + } + ConfigEvent::RewindSeconds(seconds) => { + emulation.rewind_seconds = *seconds; + } + ConfigEvent::RunAhead(run_ahead) => emulation.run_ahead = *run_ahead, + ConfigEvent::SaveSlot(slot) => emulation.save_slot = *slot, + ConfigEvent::Scale(scale) => renderer.scale = *scale, + ConfigEvent::Shader(shader) => renderer.shader = *shader, + ConfigEvent::ShowMenubar(show) => renderer.show_menubar = *show, + ConfigEvent::ShowMessages(show) => renderer.show_messages = *show, + ConfigEvent::Speed(speed) => emulation.speed = *speed, + ConfigEvent::VideoFilter(filter) => deck.filter = *filter, + ConfigEvent::ZapperConnected(connected) => deck.zapper = *connected, } + + self.renderer.prepare(&self.gamepads, &self.cfg); } - Event::WindowEvent { - window_id, event, .. - } => { - let res = self.renderer.on_window_event(window_id, &event); - if res.repaint && event != WindowEvent::RedrawRequested { - self.repaint_times.insert(window_id, Instant::now()); + NesEvent::Renderer(RendererEvent::RequestRedraw { viewport_id, when }) => { + if let Some(window_id) = self.renderer.window_id_for_viewport(viewport_id) { + self.repaint_times.insert( + window_id, + self.repaint_times + .get(&window_id) + .map_or(when, |last| (*last).min(when)), + ); } + } + NesEvent::Ui(ref event) => self.on_ui_event(event), + _ => (), + } + + // Only wake emulation of relevant events + if matches!(event, NesEvent::Emulation(_) | NesEvent::Config(_)) { + self.emulation.on_event(&event); + } + self.renderer.on_event(&event, &self.cfg); + } - if !res.consumed { - match event { - WindowEvent::RedrawRequested => { - self.emulation.try_clock_frame(); + fn resumed(&mut self, _event_loop: &ActiveEventLoop) {} - if let Err(err) = self.renderer.redraw( - window_id, - event_loop, - &mut self.gamepads, - &mut self.cfg, - ) { - self.renderer.on_error(err); - } - self.repaint_times.remove(&window_id); - } - WindowEvent::Resized(_) => { - if Some(window_id) == self.renderer.root_window_id() { - self.cfg.renderer.fullscreen = self.renderer.fullscreen(); - } - } - WindowEvent::Focused(focused) => { - if focused { - self.repaint_times.insert(window_id, Instant::now()); - if self.renderer.rom_loaded() && self.run_state.auto_paused() { - self.run_state = RunState::Running; - self.event(EmulationEvent::RunState(self.run_state)); - } - } else { - let time_since_last_save = - Instant::now() - self.renderer.last_save_time; - if time_since_last_save > Duration::from_secs(30) { - if let Err(err) = self.renderer.save(&self.cfg) { - error!("failed to save rendererer state: {err:?}"); - } - } - if self - .renderer - .window(window_id) - .and_then(|win| win.is_minimized()) - .unwrap_or(false) - { - self.repaint_times.remove(&window_id); - if self.renderer.rom_loaded() { - self.run_state = RunState::Paused; - self.event(EmulationEvent::RunState(self.run_state)); - } - } - } - } - WindowEvent::Occluded(occluded) => { - // Note: Does not trigger on all platforms (e.g. linux) - if occluded { - self.repaint_times.remove(&window_id); - if self.renderer.rom_loaded() { - self.run_state = RunState::Paused; - self.event(EmulationEvent::RunState(self.run_state)); - } - } else { - self.repaint_times.insert(window_id, Instant::now()); - if self.renderer.rom_loaded() && self.run_state.auto_paused() { - self.run_state = RunState::Running; - self.event(EmulationEvent::RunState(self.run_state)); - } - } + fn window_event( + &mut self, + event_loop: &ActiveEventLoop, + window_id: WindowId, + event: WindowEvent, + ) { + let res = self.renderer.on_window_event(window_id, &event); + if res.repaint && event != WindowEvent::RedrawRequested { + self.repaint_times.insert(window_id, Instant::now()); + } + + if !res.consumed { + match event { + WindowEvent::RedrawRequested => { + self.emulation.try_clock_frame(); + + if let Err(err) = self.renderer.redraw( + window_id, + event_loop, + &mut self.gamepads, + &mut self.cfg, + ) { + self.renderer.on_error(err); + } + self.repaint_times.remove(&window_id); + } + WindowEvent::Resized(_) => { + if Some(window_id) == self.renderer.root_window_id() { + self.cfg.renderer.fullscreen = self.renderer.fullscreen(); + } + } + WindowEvent::Focused(focused) => { + if focused { + self.repaint_times.insert(window_id, Instant::now()); + if self.renderer.rom_loaded() && self.run_state.auto_paused() { + self.run_state = RunState::Running; + self.event(EmulationEvent::RunState(self.run_state)); } - WindowEvent::KeyboardInput { event, .. } => { - if let PhysicalKey::Code(key) = event.physical_key { - self.on_input( - window_id, - Input::Key(key, self.modifiers.state()), - event.state, - event.repeat, - ); + } else { + let time_since_last_save = Instant::now() - self.renderer.last_save_time; + if time_since_last_save > Duration::from_secs(30) { + if let Err(err) = self.renderer.save(&self.cfg) { + error!("failed to save rendererer state: {err:?}"); } } - WindowEvent::ModifiersChanged(modifiers) => { - self.modifiers = modifiers; - } - WindowEvent::MouseInput { button, state, .. } => { - self.on_input(window_id, Input::Mouse(button), state, false); - } - WindowEvent::DroppedFile(path) => { - if Some(window_id) == self.renderer.root_window_id() { - self.event(EmulationEvent::LoadRomPath(path)); + if self + .renderer + .window(window_id) + .and_then(|win| win.is_minimized()) + .unwrap_or(false) + { + self.repaint_times.remove(&window_id); + if self.renderer.rom_loaded() { + self.run_state = RunState::Paused; + self.event(EmulationEvent::RunState(self.run_state)); } } - _ => (), } } - } - Event::UserEvent(event) => { - match &event { - NesEvent::Config(event) => { - let Config { - deck, - emulation, - audio, - renderer, - input, - } = &mut self.cfg; - match event { - ConfigEvent::ActionBindings(bindings) => { - input.action_bindings.clone_from(bindings); - self.input_bindings = InputBindings::from_input_config(input); - } - ConfigEvent::ActionBindingSet((action, set_input, binding)) => { - input.set_binding(*action, *set_input, *binding); - self.input_bindings.insert(*set_input, *action); - } - ConfigEvent::ActionBindingClear(clear_input) => { - input.clear_binding(*clear_input); - self.input_bindings.remove(clear_input); - } - ConfigEvent::AlwaysOnTop(always_on_top) => { - renderer.always_on_top = *always_on_top; - self.renderer - .set_always_on_top(self.cfg.renderer.always_on_top); - } - ConfigEvent::ApuChannelEnabled((channel, enabled)) => { - deck.channels_enabled[*channel as usize] = *enabled; - } - ConfigEvent::ApuChannelsEnabled(enabled) => { - deck.channels_enabled = *enabled; - } - ConfigEvent::AudioBuffer(buffer_size) => { - audio.buffer_size = *buffer_size; - } - ConfigEvent::AudioEnabled(enabled) => audio.enabled = *enabled, - ConfigEvent::AudioLatency(latency) => audio.latency = *latency, - ConfigEvent::AutoLoad(enabled) => emulation.auto_load = *enabled, - ConfigEvent::AutoSave(enabled) => emulation.auto_save = *enabled, - ConfigEvent::AutoSaveInterval(interval) => { - emulation.auto_save_interval = *interval; - } - ConfigEvent::ConcurrentDpad(enabled) => deck.concurrent_dpad = *enabled, - ConfigEvent::CycleAccurate(enabled) => deck.cycle_accurate = *enabled, - ConfigEvent::DarkTheme(enabled) => renderer.dark_theme = *enabled, - ConfigEvent::EmbedViewports(embed) => renderer.embed_viewports = *embed, - ConfigEvent::FourPlayer(four_player) => deck.four_player = *four_player, - ConfigEvent::Fullscreen(fullscreen) => { - renderer.fullscreen = *fullscreen - } - ConfigEvent::GamepadAssign((player, uuid)) => { - input.assign_gamepad(*player, *uuid); - if let Some(name) = self.gamepads.gamepad_name_by_uuid(uuid) { - self.tx.event(UiEvent::Message(( - MessageType::Info, - format!("Assigned gamepad `{name}` to player {player:?}.",), - ))); - } - } - ConfigEvent::GamepadUnassign(player) => { - if let Some(uuid) = input.unassign_gamepad(*player) { - if let Some(name) = self.gamepads.gamepad_name_by_uuid(&uuid) { - self.tx.event(UiEvent::Message(( - MessageType::Info, - format!("Unassigned gamepad `{name}` from player {player:?}."), - ))); - } - } - } - ConfigEvent::GamepadAssignments(assignments) => { - input.gamepad_assignments = *assignments; - } - ConfigEvent::GenieCodeAdded(genie_code) => { - deck.genie_codes.push(genie_code.clone()); - } - ConfigEvent::GenieCodeClear => deck.genie_codes.clear(), - ConfigEvent::GenieCodeRemoved(code) => { - deck.genie_codes.retain(|genie| genie.code() != code); - } - ConfigEvent::HideOverscan(hide) => renderer.hide_overscan = *hide, - ConfigEvent::MapperRevisions(revs) => deck.mapper_revisions = *revs, - ConfigEvent::RamState(ram_state) => deck.ram_state = *ram_state, - ConfigEvent::RecentRomsClear => renderer.recent_roms.clear(), - ConfigEvent::Region(region) => deck.region = *region, - ConfigEvent::RewindEnabled(enabled) => emulation.rewind = *enabled, - ConfigEvent::RewindInterval(interval) => { - emulation.rewind_interval = *interval; - } - ConfigEvent::RewindSeconds(seconds) => { - emulation.rewind_seconds = *seconds; - } - ConfigEvent::RunAhead(run_ahead) => emulation.run_ahead = *run_ahead, - ConfigEvent::SaveSlot(slot) => emulation.save_slot = *slot, - ConfigEvent::Scale(scale) => renderer.scale = *scale, - ConfigEvent::Shader(shader) => renderer.shader = *shader, - ConfigEvent::ShowMenubar(show) => renderer.show_menubar = *show, - ConfigEvent::ShowMessages(show) => renderer.show_messages = *show, - ConfigEvent::Speed(speed) => emulation.speed = *speed, - ConfigEvent::VideoFilter(filter) => deck.filter = *filter, - ConfigEvent::ZapperConnected(connected) => deck.zapper = *connected, + WindowEvent::Occluded(occluded) => { + // Note: Does not trigger on all platforms (e.g. linux) + if occluded { + self.repaint_times.remove(&window_id); + if self.renderer.rom_loaded() { + self.run_state = RunState::Paused; + self.event(EmulationEvent::RunState(self.run_state)); + } + } else { + self.repaint_times.insert(window_id, Instant::now()); + if self.renderer.rom_loaded() && self.run_state.auto_paused() { + self.run_state = RunState::Running; + self.event(EmulationEvent::RunState(self.run_state)); } - - self.renderer.prepare(&self.gamepads, &self.cfg); } - NesEvent::Renderer(RendererEvent::RequestRedraw { viewport_id, when }) => { - if let Some(window_id) = self.renderer.window_id_for_viewport(*viewport_id) - { - self.repaint_times.insert( + } + WindowEvent::KeyboardInput { + event, + is_synthetic, + .. + } => { + // Winit generates fake "synthetic" KeyboardInput events when the focus + // is changed to the window, or away from it. Synthetic key presses + // represent no real key presses and should be ignored. + // See https://github.com/rust-windowing/winit/issues/3543 + if !is_synthetic || event.state != ElementState::Pressed { + if let PhysicalKey::Code(key) = event.physical_key { + self.on_input( window_id, - self.repaint_times - .get(&window_id) - .map_or(*when, |last| (*last).min(*when)), + Input::Key(key, self.modifiers.state()), + event.state, + event.repeat, ); } } - NesEvent::Ui(event) => self.on_ui_event(event), - _ => (), } - - // Only wake emulation of relevant events - if matches!(event, NesEvent::Emulation(_) | NesEvent::Config(_)) { - self.emulation.on_event(&event); + WindowEvent::ModifiersChanged(modifiers) => { + self.modifiers = modifiers; } - self.renderer.on_event(&event, &self.cfg); - } - Event::LoopExiting => { - if let Err(err) = self.renderer.save(&self.cfg) { - error!("failed to save rendererer state: {err:?}"); + WindowEvent::MouseInput { button, state, .. } => { + self.on_input(window_id, Input::Mouse(button), state, false); } - self.renderer.destroy(); + WindowEvent::DroppedFile(path) => { + if Some(window_id) == self.renderer.root_window_id() { + self.event(EmulationEvent::LoadRomPath(path)); + } + } + _ => (), + } + } + } + + fn device_event( + &mut self, + _event_loop: &ActiveEventLoop, + _device_id: DeviceId, + event: DeviceEvent, + ) { + if let DeviceEvent::MouseMotion { delta } = event { + self.renderer.on_mouse_motion(delta); + } + } + + fn about_to_wait(&mut self, event_loop: &ActiveEventLoop) { + #[cfg(feature = "profiling")] + puffin::profile_function!(); - if feature!(AbortOnExit) { - panic!("exited unexpectedly"); + self.gamepads.update_events(); + if let Some(window_id) = self.renderer.root_window_id() { + let res = self.renderer.on_gamepad_update(&self.gamepads); + if res.repaint { + self.repaint_times.insert(window_id, Instant::now()); + } + + if res.consumed { + self.gamepads.clear_events(); + } else { + while let Some(event) = self.gamepads.next_event() { + self.on_gamepad_event(window_id, event); + self.repaint_times.insert(window_id, Instant::now()); } } - _ => (), } + + self.update_repaint_times(event_loop); + } + + fn suspended(&mut self, event_loop: &ActiveEventLoop) { + if feature!(Suspend) { + if let Err(err) = self.renderer.drop_window() { + error!("failed to suspend window: {err:?}"); + event_loop.exit(); + } + } + } + + fn exiting(&mut self, _event_loop: &ActiveEventLoop) { + if let Err(err) = self.renderer.save(&self.cfg) { + error!("failed to save rendererer state: {err:?}"); + } + self.renderer.destroy(); + + if feature!(AbortOnExit) { + panic!("exited unexpectedly"); + } + } + + fn memory_warning(&mut self, _event_loop: &ActiveEventLoop) { + self.renderer + .add_message(MessageType::Warn, "Your system memory is running low..."); + if self.cfg.emulation.rewind { + self.cfg.emulation.rewind = false; + self.event(ConfigEvent::RewindEnabled(false)); + } + } +} + +impl Running { + pub fn update_repaint_times(&mut self, event_loop: &ActiveEventLoop) { + let mut next_repaint_time = self.repaint_times.values().min().copied(); + self.repaint_times.retain(|window_id, when| { + if *when > Instant::now() { + return true; + } + next_repaint_time = None; + + if let Some(window) = self.renderer.window(*window_id) { + if !window.is_minimized().unwrap_or(false) { + window.request_redraw(); + } + // Repaint time will get removed as soon as we receive the RequestRedraw event + true + } else { + false + } + }); + + event_loop.set_control_flow(ControlFlow::WaitUntil(match next_repaint_time { + Some(next_repaint_time) => next_repaint_time, + None => Instant::now() + Duration::from_millis(16), + })); } pub fn on_ui_event(&mut self, event: &UiEvent) { diff --git a/tetanes/src/nes/renderer.rs b/tetanes/src/nes/renderer.rs index 1eea3c20..76f7d748 100644 --- a/tetanes/src/nes/renderer.rs +++ b/tetanes/src/nes/renderer.rs @@ -2,16 +2,13 @@ use crate::{ feature, nes::{ config::Config, - event::{ - ConfigEvent, EmulationEvent, NesEvent, NesEventProxy, RendererEvent, RunState, UiEvent, - }, + event::{EmulationEvent, NesEvent, NesEventProxy, RendererEvent, RunState, UiEvent}, input::Gamepads, renderer::{ - gui::{ - lib::{is_paste_command, key_from_keycode}, - Gui, MessageType, - }, - shader::Shader, + clipboard::Clipboard, + event::translate_cursor, + gui::{Gui, MessageType}, + painter::Painter, texture::Texture, }, }, @@ -19,13 +16,12 @@ use crate::{ thread, }; use anyhow::Context; +use crossbeam::channel::{self, Receiver}; use egui::{ - ahash::HashMap, DeferredViewportUiCallback, ImmediateViewport, SystemTheme, Vec2, - ViewportBuilder, ViewportClass, ViewportCommand, ViewportId, ViewportIdMap, ViewportIdPair, - ViewportIdSet, ViewportInfo, ViewportOutput, WindowLevel, + ahash::HashMap, DeferredViewportUiCallback, Vec2, ViewportBuilder, ViewportClass, + ViewportCommand, ViewportId, ViewportIdMap, ViewportIdPair, ViewportIdSet, ViewportInfo, + ViewportOutput, WindowLevel, }; -use egui_wgpu::{winit::Painter, RenderState}; -use egui_winit::EventResponse; use parking_lot::Mutex; use std::{cell::RefCell, collections::hash_map::Entry, rc::Rc, sync::Arc}; use tetanes_core::{ @@ -38,15 +34,17 @@ use thingbuf::{ mpsc::{blocking::Receiver as BufReceiver, errors::TryRecvError}, Recycle, }; -use tracing::{debug, error, info, warn}; +use tracing::{debug, error, info}; use winit::{ - dpi::{LogicalSize, PhysicalSize}, - event::WindowEvent, - event_loop::EventLoopWindowTarget, - window::{Theme, Window, WindowId}, + dpi::{LogicalSize, PhysicalPosition, PhysicalSize}, + event_loop::ActiveEventLoop, + window::{CursorGrabMode, Theme, Window, WindowButtons, WindowId}, }; +pub mod clipboard; +pub mod event; pub mod gui; +pub mod painter; pub mod shader; pub mod texture; @@ -68,8 +66,8 @@ impl Recycle for FrameRecycle { pub struct State { pub(crate) viewports: ViewportIdMap, viewport_from_window: HashMap, - painter: Rc>, - focused: Option, + pub(crate) focused: Option, + pub(crate) start_time: Instant, } impl std::fmt::Debug for State { @@ -78,32 +76,43 @@ impl std::fmt::Debug for State { .field("viewports", &self.viewports) .field("viewport_from_window", &self.viewport_from_window) .field("focused", &self.focused) - .finish_non_exhaustive() + .field("start_time", &self.focused) + .finish() } } +#[derive(Default)] #[must_use] pub struct Viewport { - ids: ViewportIdPair, + pub(crate) ids: ViewportIdPair, class: ViewportClass, builder: ViewportBuilder, pub(crate) info: ViewportInfo, - viewport_ui_cb: Option>, - screenshot_requested: bool, + pub(crate) raw_input: egui::RawInput, + pub(crate) viewport_ui_cb: Option>, pub(crate) window: Option>, - pub(crate) egui_state: Option, - occluded: bool, + pub(crate) occluded: bool, + cursor_icon: Option, + cursor_pos: Option, + pub(crate) clipboard: Clipboard, } impl std::fmt::Debug for Viewport { fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { f.debug_struct("Viewport") .field("ids", &self.ids) + // .field("class", &self.class) // why not?! .field("builder", &self.builder) .field("info", &self.info) - .field("screenshot_requested", &self.screenshot_requested) + .field("raw_input", &self.raw_input) + .field( + "viewport_ui_cb", + &self.viewport_ui_cb.as_ref().map(|_| "fn"), + ) .field("window", &self.window) .field("occluded", &self.occluded) + .field("cursor_icon", &self.cursor_icon) + .field("clipboard", &self.clipboard) .finish_non_exhaustive() } } @@ -111,12 +120,14 @@ impl std::fmt::Debug for Viewport { #[must_use] pub struct Renderer { pub(crate) state: Rc>, + painter: Rc>, frame_rx: BufReceiver, tx: NesEventProxy, redraw_tx: Arc>, pub(crate) gui: Rc>, pub(crate) ctx: egui::Context, - render_state: Option, + #[cfg(not(target_arch = "wasm32"))] + accesskit: accesskit_winit::Adapter, texture: Texture, first_frame: bool, pub(crate) last_save_time: Instant, @@ -128,6 +139,7 @@ impl std::fmt::Debug for Renderer { fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { f.debug_struct("Renderer") .field("state", &self.state) + .field("painter", &self.painter) .field("frame_rx", &self.frame_rx) .field("tx", &self.tx) .field("redraw_tx", &self.redraw_tx) @@ -137,6 +149,7 @@ impl std::fmt::Debug for Renderer { .field("first_frame", &self.first_frame) .field("last_save_time", &self.last_save_time) .field("zoom_changed", &self.zoom_changed) + .field("resize_texture", &self.resize_texture) .finish_non_exhaustive() } } @@ -145,7 +158,6 @@ impl std::fmt::Debug for Renderer { pub struct Resources { pub(crate) ctx: egui::Context, pub(crate) window: Arc, - pub(crate) viewport_builder: ViewportBuilder, pub(crate) painter: Painter, } @@ -153,7 +165,6 @@ impl std::fmt::Debug for Resources { fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { f.debug_struct("Resources") .field("window", &self.window) - .field("viewport_builder", &self.viewport_builder) .finish_non_exhaustive() } } @@ -162,7 +173,6 @@ impl Renderer { /// Initializes the renderer in a platform-agnostic way. pub fn new( tx: NesEventProxy, - event_loop: &EventLoopWindowTarget, resources: Resources, frame_rx: BufReceiver, cfg: &Config, @@ -170,8 +180,7 @@ impl Renderer { let Resources { ctx, window, - viewport_builder, - painter, + mut painter, } = resources; let redraw_tx = Arc::new(Mutex::new(tx.clone())); @@ -193,32 +202,10 @@ impl Renderer { // Platforms like wasm don't easily support multiple viewports, and even if it could spawn // multiple canvases for each viewport, the async requirements of wgpu would make it // impossible to render until wasm-bindgen gets proper non-blocking async/await support. - if feature!(Viewports) { + if feature!(OsViewports) { ctx.set_embed_viewports(cfg.renderer.embed_viewports); } - let max_texture_side = painter.max_texture_side(); - #[allow(unused_mut)] - let mut egui_state = egui_winit::State::new( - ctx.clone(), - ViewportId::ROOT, - &window, - Some(window.scale_factor() as f32), - max_texture_side, - ); - - #[cfg(not(target_arch = "wasm32"))] - if feature!(AccessKit) { - egui_state.init_accesskit(&window, tx.inner().clone(), { - let ctx = ctx.clone(); - move || { - ctx.enable_accesskit(); - ctx.request_repaint(); - ctx.accesskit_placeholder_tree_update() - } - }); - } - let mut viewport_from_window = HashMap::default(); viewport_from_window.insert(window.id(), ViewportId::ROOT); @@ -228,83 +215,60 @@ impl Renderer { Viewport { ids: ViewportIdPair::ROOT, class: ViewportClass::Root, - builder: viewport_builder.clone(), info: ViewportInfo { minimized: window.is_minimized(), maximized: Some(window.is_maximized()), ..Default::default() }, - viewport_ui_cb: None, - screenshot_requested: false, window: Some(Arc::clone(&window)), - egui_state: Some(egui_state), - occluded: false, + ..Default::default() }, ); - let render_state = painter.render_state(); - let (Some(max_texture_side), Some(render_state)) = (max_texture_side, render_state) else { - anyhow::bail!("render state is not initialized yet"); + painter.set_shader(cfg.renderer.shader); + let render_state = painter.render_state_mut(); + let Some(render_state) = render_state else { + anyhow::bail!("painter state is not initialized yet"); }; - let texture_size = cfg.texture_size(); let texture = Texture::new( - &render_state.device, - &mut render_state.renderer.write(), - texture_size.x.min(max_texture_side as f32) as u32, - texture_size.y.min(max_texture_side as f32) as u32, + render_state, + cfg.texture_size(), cfg.deck.region.aspect_ratio(), Some("nes frame"), ); - Self::set_shader_resource(&render_state, &texture.view, cfg.renderer.shader); - let gui = Rc::new(RefCell::new(Gui::new( tx.clone(), texture.sized_texture(), cfg.clone(), ))); - let state = Rc::new(RefCell::new(State { - viewports, - painter: Rc::new(RefCell::new(painter)), - viewport_from_window, - focused: Some(ViewportId::ROOT), - })); - - { - let tx = tx.clone(); - let state = Rc::downgrade(&state); - let event_loop: *const EventLoopWindowTarget = event_loop; - egui::Context::set_immediate_viewport_renderer(move |ctx, viewport| { - if let Some(state) = state.upgrade() { - // SAFETY: the event loop lives longer than the Rcs we just upgraded above. - match unsafe { event_loop.as_ref() } { - Some(event_loop) => { - Self::render_immediate_viewport(&tx, event_loop, ctx, &state, viewport); - } - None => tracing::error!( - "failed to get event_loop in set_immediate_viewport_renderer" - ), - } - } else { - warn!("set_immediate_viewport_renderer called after window closed"); - } - }); - } - if let Err(err) = Self::load(&ctx, cfg) { tracing::error!("{err:?}"); } + #[cfg(not(target_arch = "wasm32"))] + let accesskit = + { accesskit_winit::Adapter::with_event_loop_proxy(&window, tx.inner().clone()) }; + + let state = State { + viewports, + viewport_from_window, + focused: None, + start_time: Instant::now(), + }; + Ok(Self { - state, + state: Rc::new(RefCell::new(state)), + painter: Rc::new(RefCell::new(painter)), frame_rx, tx, redraw_tx, ctx, + #[cfg(not(target_arch = "wasm32"))] + accesskit, gui, - render_state: Some(render_state), texture, first_frame: true, last_save_time: Instant::now(), @@ -317,14 +281,11 @@ impl Renderer { let State { viewports, viewport_from_window, - painter, .. } = &mut *self.state.borrow_mut(); viewports.clear(); viewport_from_window.clear(); - let mut painter = painter.borrow_mut(); - painter.gc_viewports(&ViewportIdSet::default()); - painter.destroy(); + self.painter.borrow_mut().destroy(); } pub fn root_window_id(&self) -> Option { @@ -332,8 +293,8 @@ impl Renderer { } pub fn window_id_for_viewport(&self, viewport_id: ViewportId) -> Option { - self.state - .borrow() + let state = self.state.borrow(); + state .viewports .get(&viewport_id) .and_then(|viewport| viewport.window.as_ref()) @@ -345,16 +306,12 @@ impl Renderer { state .viewport_from_window .get(&window_id) - .and_then(|id| state.viewports.get(id)) - .map(|viewport| viewport.ids.this) + .and_then(|id| state.viewports.get(id).map(|viewport| viewport.ids.this)) } pub fn root_viewport(&self, reader: impl FnOnce(&Viewport) -> R) -> Option { - self.state - .borrow() - .viewports - .get(&ViewportId::ROOT) - .map(reader) + let state = self.state.borrow(); + state.viewports.get(&ViewportId::ROOT).map(reader) } pub fn root_window(&self) -> Option> { @@ -364,11 +321,12 @@ impl Renderer { pub fn window(&self, window_id: WindowId) -> Option> { let state = self.state.borrow(); - state - .viewport_from_window - .get(&window_id) - .and_then(|id| state.viewports.get(id)) - .and_then(|viewport| viewport.window.clone()) + state.viewport_from_window.get(&window_id).and_then(|id| { + state + .viewports + .get(id) + .and_then(|viewport| viewport.window.clone()) + }) } pub fn window_size(&self, cfg: &Config) -> Vec2 { @@ -394,11 +352,8 @@ impl Renderer { } pub fn all_viewports_occluded(&self) -> bool { - self.state - .borrow() - .viewports - .values() - .all(|viewport| viewport.occluded) + let state = self.state.borrow(); + state.viewports.values().all(|viewport| viewport.occluded) } pub fn inner_size(&self) -> Option> { @@ -406,14 +361,13 @@ impl Renderer { } pub fn fullscreen(&self) -> bool { - // viewport.info.fullscreen is sometimes stale, so rely on the actual winit state self.root_window() .map(|win| win.fullscreen().is_some()) .unwrap_or(false) } pub fn set_fullscreen(&mut self, fullscreen: bool, embed_viewports: bool) { - if feature!(Viewports) { + if feature!(OsViewports) { self.ctx.set_embed_viewports(fullscreen || embed_viewports); } self.ctx @@ -427,9 +381,8 @@ impl Renderer { } pub fn set_always_on_top(&mut self, always_on_top: bool) { - let State { viewports, .. } = &mut *self.state.borrow_mut(); - - for viewport_id in viewports.keys() { + let state = self.state.borrow(); + for viewport_id in state.viewports.keys() { self.ctx.send_viewport_cmd_to( *viewport_id, ViewportCommand::WindowLevel(if always_on_top { @@ -441,87 +394,7 @@ impl Renderer { } } - /// Handle event. - pub fn on_event(&mut self, event: &NesEvent, cfg: &Config) { - #[cfg(feature = "profiling")] - puffin::profile_function!(); - - self.gui.borrow_mut().on_event(event); - - match event { - NesEvent::Renderer(event) => match event { - RendererEvent::ViewportResized(_) => self.resize_window(cfg), - RendererEvent::ResizeTexture => self.resize_texture = true, - RendererEvent::RomLoaded(_) => { - if self.state.borrow_mut().focused != Some(ViewportId::ROOT) { - self.ctx - .send_viewport_cmd_to(ViewportId::ROOT, ViewportCommand::Focus); - } - } - _ => (), - }, - NesEvent::Config(event) => match event { - ConfigEvent::DarkTheme(enabled) => { - self.ctx.set_visuals(if *enabled { - Gui::dark_theme() - } else { - Gui::light_theme() - }); - } - ConfigEvent::EmbedViewports(embed) => { - if feature!(Viewports) { - self.ctx.set_embed_viewports(*embed); - } - } - ConfigEvent::Fullscreen(fullscreen) => { - if feature!(Viewports) { - self.ctx - .set_embed_viewports(*fullscreen || cfg.renderer.embed_viewports); - } - if self.fullscreen() != *fullscreen { - self.ctx - .send_viewport_cmd_to(ViewportId::ROOT, ViewportCommand::Focus); - self.ctx.send_viewport_cmd_to( - ViewportId::ROOT, - ViewportCommand::Fullscreen(*fullscreen), - ); - } - } - ConfigEvent::Region(_) | ConfigEvent::HideOverscan(_) | ConfigEvent::Scale(_) => { - self.resize_texture = true; - } - ConfigEvent::Shader(shader) => { - if let Some(render_state) = &self.render_state { - Self::set_shader_resource(render_state, &self.texture.view, *shader); - } - } - _ => (), - }, - #[cfg(not(target_arch = "wasm32"))] - NesEvent::AccessKit { window_id, request } => { - if feature!(AccessKit) { - let State { - viewports, - viewport_from_window, - .. - } = &mut *self.state.borrow_mut(); - let viewport_id = viewport_from_window.get(window_id); - if let Some(viewport_id) = viewport_id { - let state = viewports - .get_mut(viewport_id) - .and_then(|viewport| viewport.egui_state.as_mut()); - if let Some(state) = state { - state.on_accesskit_action_request(request.clone()); - self.ctx.request_repaint_of(*viewport_id); - } - } - } - } - _ => (), - } - } - - fn initialize_all_windows(&mut self, event_loop: &EventLoopWindowTarget) { + fn initialize_all_windows(&mut self, event_loop: &ActiveEventLoop) { #[cfg(feature = "profiling")] puffin::profile_function!(); @@ -531,18 +404,16 @@ impl Renderer { let State { viewports, - painter, viewport_from_window, .. } = &mut *self.state.borrow_mut(); - for viewport in viewports.values_mut() { viewport.initialize_window( self.tx.clone(), event_loop, &self.ctx, viewport_from_window, - painter, + &self.painter, ); } } @@ -551,133 +422,6 @@ impl Renderer { self.gui.borrow().loaded_rom.is_some() } - /// Handle window event. - pub fn on_window_event(&mut self, window_id: WindowId, event: &WindowEvent) -> EventResponse { - #[cfg(feature = "profiling")] - puffin::profile_function!(); - - let viewport_id = self.viewport_id_for_window(window_id); - match event { - WindowEvent::Focused(focused) => { - self.state.borrow_mut().focused = if *focused { viewport_id } else { None }; - } - // Note: Does not trigger on all platforms - WindowEvent::Occluded(occluded) => { - let mut state = self.state.borrow_mut(); - if let Some(viewport) = viewport_id - .as_ref() - .and_then(|id| state.viewports.get_mut(id)) - { - viewport.occluded = *occluded; - } - } - WindowEvent::CloseRequested | WindowEvent::Destroyed => { - if let Some(viewport_id) = viewport_id { - let mut state = self.state.borrow_mut(); - if viewport_id == ViewportId::ROOT { - self.tx.event(UiEvent::Terminate); - } else if let Some(viewport) = state.viewports.get_mut(&viewport_id) { - viewport.info.events.push(egui::ViewportEvent::Close); - - // We may need to repaint both us and our parent to close the window, - // and perhaps twice (once to notice the close-event, once again to enforce it). - // `request_repaint_of` does a double-repaint though: - self.ctx.request_repaint_of(viewport_id); - self.ctx.request_repaint_of(viewport.ids.parent); - } - } - } - // To support clipboard in wasm, we need to intercept the Paste event so that - // egui_winit doesn't try to use it's clipboard fallback logic for paste. Associated - // behavior in the wasm platform layer handles setting the egui_state clipboard text. - WindowEvent::KeyboardInput { - event: - winit::event::KeyEvent { - physical_key: winit::keyboard::PhysicalKey::Code(key), - .. - }, - .. - } => { - if let Some(key) = key_from_keycode(*key) { - use egui::Key; - - let modifiers = self.ctx.input(|i| i.modifiers); - - if feature!(ConsumePaste) && is_paste_command(modifiers, key) { - return EventResponse { - consumed: true, - repaint: true, - }; - } - - if matches!(key, Key::Plus | Key::Equals | Key::Minus | Key::Num0) - && (modifiers.ctrl || modifiers.command) - { - self.zoom_changed = true; - } - } - } - WindowEvent::Resized(size) => { - if let Some(viewport_id) = viewport_id { - use std::num::NonZeroU32; - if let (Some(width), Some(height)) = - (NonZeroU32::new(size.width), NonZeroU32::new(size.height)) - { - self.state - .borrow_mut() - .painter - .borrow_mut() - .on_window_resized(viewport_id, width, height); - } - } - } - WindowEvent::ThemeChanged(theme) => { - self.ctx - .send_viewport_cmd(ViewportCommand::SetTheme(if *theme == Theme::Light { - SystemTheme::Light - } else { - SystemTheme::Dark - })); - } - _ => (), - } - - let mut state = self.state.borrow_mut(); - let mut res = viewport_id - .and_then(|viewport_id| { - state.viewports.get_mut(&viewport_id).and_then(|viewport| { - Some( - viewport - .egui_state - .as_mut()? - .on_window_event(viewport.window.as_deref()?, event), - ) - }) - }) - .unwrap_or_default(); - - let gui_res = self.gui.borrow_mut().on_window_event(event); - res.consumed |= gui_res.consumed; - res.repaint |= gui_res.repaint; - - res - } - - /// Handle gamepad event updates. - pub fn on_gamepad_update(&self, gamepads: &Gamepads) -> EventResponse { - #[cfg(feature = "profiling")] - puffin::profile_function!(); - - if self.gui.borrow().keybinds.wants_input() && gamepads.has_events() { - EventResponse { - consumed: true, - repaint: true, - } - } else { - EventResponse::default() - } - } - pub fn add_message(&mut self, ty: MessageType, text: S) where S: Into, @@ -728,42 +472,169 @@ impl Renderer { Ok(()) } - pub fn create_window( - event_loop: &EventLoopWindowTarget, - ctx: &egui::Context, + /// Request renderer resources (creating gui context, window, painter, etc). + /// + /// # Errors + /// + /// Returns an error if any resources can't be created correctly or `init_running` has already + /// been called. + pub fn request_resources( + event_loop: &ActiveEventLoop, + tx: &NesEventProxy, cfg: &Config, - ) -> anyhow::Result<(Window, ViewportBuilder)> { + ) -> anyhow::Result<(egui::Context, Arc, Receiver)> { + let ctx = egui::Context::default(); + let window_size = cfg.window_size(cfg.deck.region.aspect_ratio()); - let mut viewport_builder = ViewportBuilder::default() - .with_app_id(Config::WINDOW_TITLE) + let mut builder = egui::ViewportBuilder::default() .with_title(Config::WINDOW_TITLE) - .with_active(true) .with_visible(false) // hide until first frame is rendered. required by AccessKit - .with_inner_size(window_size) - .with_min_inner_size(Vec2::new(Ppu::WIDTH as f32, Ppu::HEIGHT as f32)) .with_fullscreen(cfg.renderer.fullscreen) - .with_resizable(true); + .with_active(true) + .with_resizable(true) + .with_inner_size(window_size) + .with_min_inner_size(Vec2::new(Ppu::WIDTH as f32, Ppu::HEIGHT as f32)); if cfg.renderer.always_on_top { - viewport_builder = viewport_builder.with_always_on_top(); + builder = builder.with_always_on_top(); } + let window = Arc::new(Self::create_window(&ctx, event_loop, builder)?); + window.set_theme(Some(if cfg.renderer.dark_theme { + Theme::Dark + } else { + Theme::Light + })); + + let (painter_tx, painter_rx) = channel::bounded(1); + thread::spawn({ + let window = Arc::clone(&window); + let event_tx = tx.clone(); + async move { + debug!("creating painter..."); + match Self::create_painter(window).await { + Ok(painter) => { + painter_tx.send(painter).expect("failed to send painter"); + event_tx.event(RendererEvent::ResourcesReady); + } + Err(err) => { + error!("failed to create painter: {err:?}"); + event_tx.event(UiEvent::Terminate); + } + } + } + }); - let window_builder = - egui_winit::create_winit_window_builder(ctx, event_loop, viewport_builder.clone()); + Ok((ctx, window, painter_rx)) + } - let window = window_builder - .with_platform(Config::WINDOW_TITLE) - .with_theme(Some(if cfg.renderer.dark_theme { - Theme::Dark - } else { - Theme::Light - })) - .build(event_loop)?; + pub fn create_window( + ctx: &egui::Context, + event_loop: &ActiveEventLoop, + builder: ViewportBuilder, + ) -> anyhow::Result { + let native_pixels_per_point = event_loop + .primary_monitor() + .or_else(|| event_loop.available_monitors().next()) + .map_or_else( + || { + tracing::debug!( + "Failed to find a monitor - assuming native_pixels_per_point of 1.0" + ); + 1.0 + }, + |m| m.scale_factor() as f32, + ); + let zoom_factor = ctx.zoom_factor(); + let pixels_per_point = zoom_factor * native_pixels_per_point; + + let ViewportBuilder { + title, + position, + inner_size, + min_inner_size, + max_inner_size, + fullscreen, + maximized, + resizable, + icon, + active, + visible, + window_level, + .. + } = builder; + + let title = title.unwrap_or_else(|| Config::WINDOW_TITLE.to_owned()); + let mut window_attrs = Window::default_attributes() + .with_title(title.clone()) + .with_resizable(resizable.unwrap_or(true)) + .with_visible(visible.unwrap_or(true)) + .with_maximized(maximized.unwrap_or(false)) + .with_window_level(match window_level.unwrap_or_default() { + WindowLevel::AlwaysOnBottom => winit::window::WindowLevel::AlwaysOnBottom, + WindowLevel::AlwaysOnTop => winit::window::WindowLevel::AlwaysOnTop, + WindowLevel::Normal => winit::window::WindowLevel::Normal, + }) + .with_fullscreen( + fullscreen.and_then(|e| e.then_some(winit::window::Fullscreen::Borderless(None))), + ) + .with_active(active.unwrap_or(true)) + .with_platform(&title); + + if let Some(size) = inner_size { + window_attrs = window_attrs.with_inner_size(PhysicalSize::new( + pixels_per_point * size.x, + pixels_per_point * size.y, + )); + } + + if let Some(size) = min_inner_size { + window_attrs = window_attrs.with_min_inner_size(PhysicalSize::new( + pixels_per_point * size.x, + pixels_per_point * size.y, + )); + } - egui_winit::apply_viewport_builder_to_window(ctx, &window, &viewport_builder); + if let Some(size) = max_inner_size { + window_attrs = window_attrs.with_max_inner_size(PhysicalSize::new( + pixels_per_point * size.x, + pixels_per_point * size.y, + )); + } + + if let Some(pos) = position { + window_attrs = window_attrs.with_position(PhysicalPosition::new( + pixels_per_point * pos.x, + pixels_per_point * pos.y, + )); + } + + if let Some(icon) = icon { + let winit_icon = gui::lib::to_winit_icon(&icon); + window_attrs = window_attrs.with_window_icon(winit_icon); + } + + let window = event_loop.create_window(window_attrs)?; + + if let Some(size) = inner_size { + if window + .request_inner_size(PhysicalSize::new( + pixels_per_point * size.x, + pixels_per_point * size.y, + )) + .is_some() + { + debug!("Failed to set window size"); + } + } + if let Some(size) = min_inner_size { + window.set_min_inner_size(Some(PhysicalSize::new( + pixels_per_point * size.x, + pixels_per_point * size.y, + ))); + } debug!("created new window: {:?}", window.id()); - Ok((window, viewport_builder)) + Ok(window) } pub async fn create_painter(window: Arc) -> anyhow::Result { @@ -782,64 +653,15 @@ impl Renderer { start.elapsed().as_secs_f32() ); - let mut painter = Painter::new(egui_wgpu::WgpuConfiguration::default(), 1, None, false); - - // Creating device may fail if adapter doesn't support our requested cfg above, so try to - // recover with lower limits. Specifically max_texture_dimension_2d has a downlevel default - // of 2048. egui_wgpu wants 8192 for 4k displays, but not all platforms support that yet. - if let Err(err) = painter + let mut painter = Painter::new(); + painter .set_window(ViewportId::ROOT, Some(Arc::clone(&window))) - .await - { - if let egui_wgpu::WgpuError::RequestDeviceError(_) = err { - painter = Painter::new( - egui_wgpu::WgpuConfiguration { - device_descriptor: Arc::new(|adapter| { - let base_limits = if adapter.get_info().backend == wgpu::Backend::Gl { - wgpu::Limits::downlevel_webgl2_defaults() - } else { - wgpu::Limits::default() - }; - wgpu::DeviceDescriptor { - label: Some("egui wgpu device"), - required_features: wgpu::Features::default(), - required_limits: wgpu::Limits { - max_texture_dimension_2d: 4096, - ..base_limits - }, - } - }), - ..Default::default() - }, - 1, - None, - false, - ); - painter.set_window(ViewportId::ROOT, Some(window)).await?; - } else { - return Err(err.into()); - } - } - - let adapter_info = painter.render_state().map(|state| state.adapter.get_info()); - if let Some(info) = adapter_info { - debug!( - "created new painter for adapter: `{}`. backend: `{}`", - if info.name.is_empty() { - "unknown" - } else { - &info.name - }, - info.backend.to_str() - ); - } else { - debug!("created new painter. Adapter unknown."); - } + .await?; Ok(painter) } - pub fn recreate_window(&mut self, event_loop: &EventLoopWindowTarget) { + pub fn recreate_window(&mut self, event_loop: &ActiveEventLoop) { if self.ctx.embed_viewports() { return; } @@ -847,11 +669,9 @@ impl Renderer { let State { viewports, viewport_from_window, - painter, .. } = &mut *self.state.borrow_mut(); - - let viewport_builder = viewports + let builder = viewports .get(&ViewportId::ROOT) .map(|viewport| viewport.builder.clone()) .unwrap_or_default(); @@ -860,8 +680,7 @@ impl Renderer { viewports, ViewportIdPair::ROOT, ViewportClass::Root, - viewport_builder, - None, + builder, None, ); @@ -870,11 +689,11 @@ impl Renderer { event_loop, &self.ctx, viewport_from_window, - painter, + &self.painter, ); } - pub fn drop_window(&mut self) -> Result<(), egui_wgpu::WgpuError> { + pub fn drop_window(&mut self) -> anyhow::Result<()> { if self.ctx.embed_viewports() { return Ok(()); } @@ -882,7 +701,7 @@ impl Renderer { state.viewports.remove(&ViewportId::ROOT); Renderer::set_painter_window( self.tx.clone(), - Rc::clone(&state.painter), + Rc::clone(&self.painter), ViewportId::ROOT, None, ); @@ -913,7 +732,6 @@ impl Renderer { class: ViewportClass, mut builder: ViewportBuilder, viewport_ui_cb: Option>, - focused: Option, ) -> &'a mut Viewport { #[cfg(feature = "profiling")] puffin::profile_function!(); @@ -929,12 +747,8 @@ impl Renderer { ids, class, builder, - info: Default::default(), viewport_ui_cb, - screenshot_requested: false, - window: None, - egui_state: None, - occluded: false, + ..Default::default() }), Entry::Occupied(mut entry) => { let viewport = entry.get_mut(); @@ -945,16 +759,14 @@ impl Renderer { let (delta_commands, recreate) = viewport.builder.patch(builder); if recreate { viewport.window = None; - viewport.egui_state = None; + viewport.raw_input = Default::default(); + viewport.cursor_icon = None; } else if let Some(window) = &viewport.window { - let is_viewport_focused = focused == Some(ids.this); - egui_winit::process_viewport_commands( + Self::process_viewport_commands( ctx, &mut viewport.info, delta_commands, window, - is_viewport_focused, - &mut viewport.screenshot_requested, ); } @@ -963,11 +775,35 @@ impl Renderer { } } + pub fn handle_platform_output(viewport: &mut Viewport, platform_output: egui::PlatformOutput) { + let egui::PlatformOutput { + cursor_icon, + open_url, + copied_text, + .. + } = platform_output; + + viewport.set_cursor(cursor_icon); + + if let Some(open_url) = open_url { + Self::open_url_in_browser(&open_url.url); + } + + if !copied_text.is_empty() { + viewport.clipboard.set(copied_text); + } + } + + fn open_url_in_browser(url: &str) { + if let Err(err) = webbrowser::open(url) { + tracing::warn!("failed to open url: {err:?}"); + } + } + fn handle_viewport_output( ctx: &egui::Context, viewports: &mut ViewportIdMap, outputs: ViewportIdMap, - focused: Option, ) { #[cfg(feature = "profiling")] puffin::profile_function!(); @@ -981,129 +817,226 @@ impl Renderer { output.class, output.builder, output.viewport_ui_cb, - focused, ); if let Some(window) = viewport.window.as_ref() { - let is_viewport_focused = focused == Some(id); - egui_winit::process_viewport_commands( - ctx, - &mut viewport.info, - output.commands, - window, - is_viewport_focused, - &mut viewport.screenshot_requested, - ); + Self::process_viewport_commands(ctx, &mut viewport.info, output.commands, window); } } } - fn render_immediate_viewport( - tx: &NesEventProxy, - event_loop: &EventLoopWindowTarget, + fn process_viewport_commands( ctx: &egui::Context, - state: &RefCell, - immediate_viewport: ImmediateViewport<'_>, + info: &mut ViewportInfo, + commands: impl IntoIterator, + window: &Window, ) { - #[cfg(feature = "profiling")] - puffin::profile_function!(); - - let ImmediateViewport { - ids, - builder, - viewport_ui_cb, - } = immediate_viewport; - - let input = { - let State { - viewports, - painter, - viewport_from_window, - .. - } = &mut *state.borrow_mut(); - - let viewport = Self::create_or_update_viewport( - ctx, - viewports, - ids, - ViewportClass::Immediate, - builder, - None, - None, - ); - - if viewport.window.is_none() { - viewport.initialize_window( - tx.clone(), - event_loop, - ctx, - viewport_from_window, - painter, - ); - } - - match (&viewport.window, &mut viewport.egui_state) { - (Some(window), Some(egui_state)) => { - egui_winit::update_viewport_info(&mut viewport.info, ctx, window); - - let mut input = egui_state.take_egui_input(window); - input.viewports = viewports - .iter() - .map(|(id, viewport)| (*id, viewport.info.clone())) - .collect(); - input + let pixels_per_point = gui::lib::pixels_per_point(ctx, window); + for command in commands { + match command { + ViewportCommand::Close => { + info.events.push(egui::ViewportEvent::Close); + } + ViewportCommand::StartDrag => { + // If `.has_focus()` is not checked on x11 the input will be permanently taken until the app is killed! + if window.has_focus() { + if let Err(err) = window.drag_window() { + tracing::warn!("{command:?}: {err}"); + } + } + } + ViewportCommand::InnerSize(size) => { + let width_px = pixels_per_point * size.x.max(1.0); + let height_px = pixels_per_point * size.y.max(1.0); + let requested_size = PhysicalSize::new(width_px, height_px); + if let Some(_returned_inner_size) = window.request_inner_size(requested_size) { + // On platforms where the size is entirely controlled by the user the + // applied size will be returned immediately, resize event in such case + // may not be generated. + // e.g. Linux + + // On platforms where resizing is disallowed by the windowing system, the current + // inner size is returned immediately, and the user one is ignored. + // e.g. Android, iOS, … + + // However, comparing the results is prone to numerical errors + // because the linux backend converts physical to logical and back again. + // So let's just assume it worked: + + info.inner_rect = gui::lib::inner_rect_in_points(window, pixels_per_point); + info.outer_rect = gui::lib::outer_rect_in_points(window, pixels_per_point); + } else { + // e.g. macOS, Windows + // The request went to the display system, + // and the actual size will be delivered later with the [`WindowEvent::Resized`]. + } + } + ViewportCommand::BeginResize(direction) => { + use egui::viewport::ResizeDirection as EguiResizeDirection; + use winit::window::ResizeDirection; + + if let Err(err) = window.drag_resize_window(match direction { + EguiResizeDirection::North => ResizeDirection::North, + EguiResizeDirection::South => ResizeDirection::South, + EguiResizeDirection::East => ResizeDirection::East, + EguiResizeDirection::West => ResizeDirection::West, + EguiResizeDirection::NorthEast => ResizeDirection::NorthEast, + EguiResizeDirection::SouthEast => ResizeDirection::SouthEast, + EguiResizeDirection::NorthWest => ResizeDirection::NorthWest, + EguiResizeDirection::SouthWest => ResizeDirection::SouthWest, + }) { + tracing::warn!("{command:?}: {err}"); + } + } + ViewportCommand::Title(title) => { + window.set_title(&title); + } + ViewportCommand::Transparent(v) => window.set_transparent(v), + ViewportCommand::Visible(v) => window.set_visible(v), + ViewportCommand::OuterPosition(pos) => { + window.set_outer_position(PhysicalPosition::new( + pixels_per_point * pos.x, + pixels_per_point * pos.y, + )); + } + ViewportCommand::MinInnerSize(s) => { + window.set_min_inner_size((s.is_finite() && s != Vec2::ZERO).then_some( + PhysicalSize::new(pixels_per_point * s.x, pixels_per_point * s.y), + )); + } + ViewportCommand::MaxInnerSize(s) => { + window.set_max_inner_size((s.is_finite() && s != Vec2::INFINITY).then_some( + PhysicalSize::new(pixels_per_point * s.x, pixels_per_point * s.y), + )); + } + ViewportCommand::ResizeIncrements(s) => { + window.set_resize_increments(s.map(|s| { + PhysicalSize::new(pixels_per_point * s.x, pixels_per_point * s.y) + })); + } + ViewportCommand::Resizable(v) => window.set_resizable(v), + ViewportCommand::EnableButtons { + close, + minimized, + maximize, + } => window.set_enabled_buttons( + if close { + WindowButtons::CLOSE + } else { + WindowButtons::empty() + } | if minimized { + WindowButtons::MINIMIZE + } else { + WindowButtons::empty() + } | if maximize { + WindowButtons::MAXIMIZE + } else { + WindowButtons::empty() + }, + ), + ViewportCommand::Minimized(v) => { + window.set_minimized(v); + info.minimized = Some(v); + } + ViewportCommand::Maximized(v) => { + window.set_maximized(v); + info.maximized = Some(v); + } + ViewportCommand::Fullscreen(v) => { + window.set_fullscreen(v.then_some(winit::window::Fullscreen::Borderless(None))); + info.fullscreen = Some(v); + } + ViewportCommand::Decorations(v) => window.set_decorations(v), + ViewportCommand::WindowLevel(l) => { + use egui::viewport::WindowLevel as EguiWindowLevel; + use winit::window::WindowLevel; + window.set_window_level(match l { + EguiWindowLevel::AlwaysOnBottom => WindowLevel::AlwaysOnBottom, + EguiWindowLevel::AlwaysOnTop => WindowLevel::AlwaysOnTop, + EguiWindowLevel::Normal => WindowLevel::Normal, + }); + } + ViewportCommand::Icon(icon) => { + let winit_icon = icon.and_then(|icon| gui::lib::to_winit_icon(&icon)); + window.set_window_icon(winit_icon); + } + ViewportCommand::IMERect(rect) => { + window.set_ime_cursor_area( + PhysicalPosition::new( + pixels_per_point * rect.min.x, + pixels_per_point * rect.min.y, + ), + PhysicalSize::new( + pixels_per_point * rect.size().x, + pixels_per_point * rect.size().y, + ), + ); } - _ => return, + ViewportCommand::IMEAllowed(v) => window.set_ime_allowed(v), + ViewportCommand::IMEPurpose(p) => window.set_ime_purpose(match p { + egui::viewport::IMEPurpose::Password => winit::window::ImePurpose::Password, + egui::viewport::IMEPurpose::Terminal => winit::window::ImePurpose::Terminal, + egui::viewport::IMEPurpose::Normal => winit::window::ImePurpose::Normal, + }), + ViewportCommand::Focus => { + if !window.has_focus() { + window.focus_window(); + } + } + ViewportCommand::RequestUserAttention(a) => { + window.request_user_attention(match a { + egui::UserAttentionType::Reset => None, + egui::UserAttentionType::Critical => { + Some(winit::window::UserAttentionType::Critical) + } + egui::UserAttentionType::Informational => { + Some(winit::window::UserAttentionType::Informational) + } + }); + } + ViewportCommand::SetTheme(t) => window.set_theme(match t { + egui::SystemTheme::Light => Some(winit::window::Theme::Light), + egui::SystemTheme::Dark => Some(winit::window::Theme::Dark), + egui::SystemTheme::SystemDefault => None, + }), + ViewportCommand::ContentProtected(v) => window.set_content_protected(v), + ViewportCommand::CursorPosition(pos) => { + if let Err(err) = window.set_cursor_position(PhysicalPosition::new( + pixels_per_point * pos.x, + pixels_per_point * pos.y, + )) { + tracing::warn!("{command:?}: {err}"); + } + } + ViewportCommand::CursorGrab(o) => { + if let Err(err) = window.set_cursor_grab(match o { + egui::viewport::CursorGrab::None => CursorGrabMode::None, + egui::viewport::CursorGrab::Confined => CursorGrabMode::Confined, + egui::viewport::CursorGrab::Locked => CursorGrabMode::Locked, + }) { + tracing::warn!("{command:?}: {err}"); + } + } + ViewportCommand::CursorVisible(v) => window.set_cursor_visible(v), + ViewportCommand::MousePassthrough(passthrough) => { + if let Err(err) = window.set_cursor_hittest(!passthrough) { + tracing::warn!("{command:?}: {err}"); + } + } + _ => (), } - }; - - let output = ctx.run(input, |ctx| { - viewport_ui_cb(ctx); - }); - let viewport_id = ids.this; - let State { - viewports, - painter, - focused, - .. - } = &mut *state.borrow_mut(); - - if let Some(viewport) = viewports.get_mut(&viewport_id) { - viewport.info.events.clear(); - - if let (Some(window), Some(egui_state)) = (&viewport.window, &mut viewport.egui_state) { - Renderer::set_painter_window( - tx.clone(), - Rc::clone(painter), - viewport_id, - Some(Arc::clone(window)), - ); - - let clipped_primitives = ctx.tessellate(output.shapes, output.pixels_per_point); - painter.borrow_mut().paint_and_update_textures( - viewport_id, - output.pixels_per_point, - [0.0; 4], - &clipped_primitives, - &output.textures_delta, - false, - ); - - egui_state.handle_platform_output(window, output.platform_output); - Self::handle_viewport_output(ctx, viewports, output.viewport_output, *focused); - }; - }; + } } pub fn prepare(&mut self, gamepads: &Gamepads, cfg: &Config) { self.gui.borrow_mut().prepare(gamepads, cfg); - self.ctx.request_repaint(); + self.ctx.request_repaint(); // Ensure any windows relying on cfg are repainted } /// Request redraw. pub fn redraw( &mut self, window_id: WindowId, - event_loop: &EventLoopWindowTarget, + event_loop: &ActiveEventLoop, gamepads: &mut Gamepads, cfg: &mut Config, ) -> anyhow::Result<()> { @@ -1132,7 +1065,11 @@ impl Renderer { self.handle_resize(viewport_id, cfg); let (viewport_ui_cb, raw_input) = { - let State { viewports, .. } = &mut *self.state.borrow_mut(); + let State { + viewports, + start_time, + .. + } = &mut *self.state.borrow_mut(); let Some(viewport) = viewports.get_mut(&viewport_id) else { return Ok(()); @@ -1140,24 +1077,27 @@ impl Renderer { let Some(window) = &viewport.window else { return Ok(()); }; - - if viewport.occluded - || (viewport_id != ViewportId::ROOT && viewport.viewport_ui_cb.is_none()) - { - // This will only happen if this is an immediate viewport. - // That means that the viewport cannot be rendered by itself and needs his parent to be rendered. + if viewport.occluded { return Ok(()); } - egui_winit::update_viewport_info(&mut viewport.info, &self.ctx, window); + Viewport::update_info(&mut viewport.info, &self.ctx, window); let viewport_ui_cb = viewport.viewport_ui_cb.clone(); - let egui_state = viewport - .egui_state - .as_mut() - .context("failed to get egui_state")?; - let mut raw_input = egui_state.take_egui_input(window); + // On Windows, a minimized window will have 0 width and height. + // See: https://github.com/rust-windowing/winit/issues/208 + // This solves an issue where egui window positions would be changed when minimizing on Windows. + let screen_size_in_pixels = gui::lib::screen_size_in_pixels(window); + let screen_size_in_points = + screen_size_in_pixels / gui::lib::pixels_per_point(&self.ctx, window); + + let mut raw_input = viewport.raw_input.take(); + raw_input.time = Some(start_time.elapsed().as_secs_f64()); + raw_input.screen_rect = (screen_size_in_points.x > 0.0 + && screen_size_in_points.y > 0.0) + .then(|| egui::Rect::from_min_size(egui::Pos2::ZERO, screen_size_in_points)); + raw_input.viewport_id = viewport_id; raw_input.viewports = viewports .iter() .map(|(id, viewport)| (*id, viewport.info.clone())) @@ -1169,7 +1109,7 @@ impl Renderer { // Copy NES frame buffer before drawing UI because a UI interaction might cause a texture // resize tied to a configuration change. if viewport_id == ViewportId::ROOT { - if let Some(render_state) = &self.render_state { + if let Some(render_state) = &self.painter.borrow().render_state() { let mut frame_buffer = self.frame_rx.try_recv_ref(); while self.frame_rx.remaining() < 2 { debug!("skipping frame"); @@ -1205,19 +1145,16 @@ impl Renderer { } } - let output = self.ctx.run(raw_input, |ctx| match viewport_ui_cb { + // Mutated by accesskit below on platforms that support it + #[allow(unused_mut)] + let mut output = self.ctx.run(raw_input, |ctx| match viewport_ui_cb { Some(viewport_ui_cb) => viewport_ui_cb(ctx), None => self.gui.borrow_mut().ui(ctx, Some(gamepads)), }); { - // Required to get mutable reference again to avoid double borrow when calling gui.ui - // above because internally gui.ui calls show_viewport_immediate, which requires - // borrowing state to render let State { viewports, - painter, - focused, viewport_from_window, .. } = &mut *self.state.borrow_mut(); @@ -1230,45 +1167,51 @@ impl Renderer { let Viewport { window: Some(window), - egui_state: Some(egui_state), - screenshot_requested, .. } = viewport else { return Ok(()); }; - window.pre_present_notify(); - let clipped_primitives = self.ctx.tessellate(output.shapes, output.pixels_per_point); - let screenshot_requested = std::mem::take(screenshot_requested); - painter.borrow_mut().paint_and_update_textures( + + window.pre_present_notify(); + self.painter.borrow_mut().paint( viewport_id, output.pixels_per_point, - [0.0; 4], &clipped_primitives, &output.textures_delta, - screenshot_requested, ); if std::mem::take(&mut self.first_frame) { window.set_visible(true); } - let active_viewports_ids: ViewportIdSet = - output.viewport_output.keys().copied().collect(); + let active_viewports_ids = output + .viewport_output + .keys() + .copied() + .collect::(); if feature!(ScreenReader) && self.ctx.options(|o| o.screen_reader) { platform::speak_text(&output.platform_output.events_description()); } + // TODO: Update accesskit when egui supports an updated version + // #[cfg(not(target_arch = "wasm32"))] + // if let Some(update) = output.platform_output.accesskit_update.take() { + // tracing::trace!("update accesskit: {update:?}"); + // self.accesskit.update_if_active(|| update); + // } - egui_state.handle_platform_output(window, output.platform_output); - Self::handle_viewport_output(&self.ctx, viewports, output.viewport_output, *focused); + Self::handle_platform_output(viewport, output.platform_output); + Self::handle_viewport_output(&self.ctx, viewports, output.viewport_output); // Prune dead viewports viewports.retain(|id, _| active_viewports_ids.contains(id)); viewport_from_window.retain(|_, id| active_viewports_ids.contains(id)); - painter.borrow_mut().gc_viewports(&active_viewports_ids); + self.painter + .borrow_mut() + .retain_surfaces(&active_viewports_ids); if viewport_id == ViewportId::ROOT { for (viewport_id, viewport) in viewports { @@ -1298,24 +1241,14 @@ impl Renderer { if viewport_id == ViewportId::ROOT && self.resize_texture { tracing::debug!("resizing window and texture"); + self.tx.event(EmulationEvent::RequestFrame); self.resize_window(cfg); - let State { painter, .. } = &mut *self.state.borrow_mut(); - - if let (Some(max_texture_side), Some(render_state)) = - (painter.borrow().max_texture_side(), &self.render_state) - { + if let Some(render_state) = self.painter.borrow_mut().render_state_mut() { let texture_size = cfg.texture_size(); - self.texture.resize( - &render_state.device, - &mut render_state.renderer.write(), - texture_size.x.min(max_texture_side as f32) as u32, - texture_size.y.min(max_texture_side as f32) as u32, - self.gui.borrow().aspect_ratio(), - ); + self.texture + .resize(render_state, texture_size, self.gui.borrow().aspect_ratio()); self.gui.borrow_mut().texture = self.texture.sized_texture(); - - Self::set_shader_resource(render_state, &self.texture.view, cfg.renderer.shader); } self.resize_texture = false; } @@ -1354,30 +1287,13 @@ impl Renderer { } } } - - fn set_shader_resource(render_state: &RenderState, view: &wgpu::TextureView, shader: Shader) { - if matches!(shader, Shader::None) { - render_state - .renderer - .write() - .callback_resources - .remove::(); - } else { - let shader_resource = shader::Resources::new(render_state, view, shader); - render_state - .renderer - .write() - .callback_resources - .insert(shader_resource); - } - } } impl Viewport { pub fn initialize_window( &mut self, tx: NesEventProxy, - event_loop: &EventLoopWindowTarget, + event_loop: &ActiveEventLoop, ctx: &egui::Context, viewport_from_window: &mut HashMap, painter: &Rc>, @@ -1390,14 +1306,9 @@ impl Viewport { puffin::profile_function!(); let viewport_id = self.ids.this; - let window_builder = - egui_winit::create_winit_window_builder(ctx, event_loop, self.builder.clone()) - .with_platform(self.builder.title.as_deref().unwrap_or_default()); - match window_builder.build(event_loop) { + match Renderer::create_window(ctx, event_loop, self.builder.clone()) { Ok(window) => { - egui_winit::apply_viewport_builder_to_window(ctx, &window, &self.builder); - viewport_from_window.insert(window.id(), viewport_id); let window = Arc::new(window); @@ -1410,14 +1321,6 @@ impl Viewport { debug!("created new viewport window: {:?}", window.id()); - self.egui_state = Some(egui_winit::State::new( - ctx.clone(), - viewport_id, - event_loop, - Some(window.scale_factor() as f32), - painter.borrow().max_texture_side(), - )); - self.info.minimized = window.is_minimized(); self.info.maximized = Some(window.is_maximized()); self.window = Some(window); @@ -1425,4 +1328,62 @@ impl Viewport { Err(err) => error!("Failed to create window: {err}"), } } + + pub fn update_info(info: &mut ViewportInfo, ctx: &egui::Context, window: &Window) { + let pixels_per_point = gui::lib::pixels_per_point(ctx, window); + let has_position = window.is_minimized().map_or(true, |minimized| !minimized); + + let inner_rect = has_position + .then(|| gui::lib::inner_rect_in_points(window, pixels_per_point)) + .flatten(); + let outer_rect = has_position + .then(|| gui::lib::outer_rect_in_points(window, pixels_per_point)) + .flatten(); + + let monitor_size = window.current_monitor().map(|monitor| { + let size = monitor.size().to_logical::(pixels_per_point.into()); + egui::vec2(size.width, size.height) + }); + + info.title = Some(window.title()); + info.native_pixels_per_point = Some(window.scale_factor() as f32); + + info.monitor_size = monitor_size; + info.inner_rect = inner_rect; + info.outer_rect = outer_rect; + + if !cfg!(target_os = "macos") { + // Asking for minimized/maximized state at runtime can lead to a deadlock on macOS + info.maximized = Some(window.is_maximized()); + info.minimized = Some(window.is_minimized().unwrap_or(false)); + } + + info.fullscreen = Some(window.fullscreen().is_some()); + info.focused = Some(window.has_focus()); + } + + fn set_cursor(&mut self, cursor_icon: egui::CursorIcon) { + if self.cursor_icon == Some(cursor_icon) { + // Prevent flickering near frame boundary when Windows OS tries to control cursor icon for window resizing. + // On other platforms: just early-out to save CPU. + return; + } + let Some(window) = &self.window else { + return; + }; + + let is_pointer_in_window = self.cursor_pos.is_some(); + if is_pointer_in_window { + self.cursor_icon = Some(cursor_icon); + + if let Some(cursor) = translate_cursor(cursor_icon) { + window.set_cursor_visible(true); + window.set_cursor(cursor); + } else { + window.set_cursor_visible(false); + } + } else { + self.cursor_icon = None; + } + } } diff --git a/tetanes/src/nes/renderer/clipboard.rs b/tetanes/src/nes/renderer/clipboard.rs new file mode 100644 index 00000000..b8f33485 --- /dev/null +++ b/tetanes/src/nes/renderer/clipboard.rs @@ -0,0 +1,60 @@ +#[must_use] +pub struct Clipboard { + #[cfg(not(target_arch = "wasm32"))] + inner: Option, + /// Fallback. + text: String, +} + +impl Default for Clipboard { + #[allow(clippy::derivable_impls)] + fn default() -> Self { + Self { + #[cfg(not(target_arch = "wasm32"))] + inner: arboard::Clipboard::new() + .map_err(|err| tracing::warn!("failed to initialize clipboard: {err:?}")) + .ok(), + text: String::new(), + } + } +} + +impl std::fmt::Debug for Clipboard { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + let mut res = f.debug_struct("Clipboard"); + #[cfg(not(target_arch = "wasm32"))] + res.field("inner", &self.inner.as_ref().map(|_| "arboard")); + res.field("text", &self.text).finish_non_exhaustive() + } +} + +impl Clipboard { + pub fn new() -> Self { + Self::default() + } + + pub fn get(&mut self) -> Option { + #[cfg(not(target_arch = "wasm32"))] + if let Some(inner) = self.inner.as_mut() { + return inner + .get_text() + .map_err(|err| tracing::warn!("clipboard paste error: {err:?}")) + .ok(); + } + + Some(self.text.clone()) + } + + pub fn set(&mut self, text: impl Into) { + let text = text.into(); + #[cfg(not(target_arch = "wasm32"))] + if let Some(inner) = self.inner.as_mut() { + if let Err(err) = inner.set_text(text) { + tracing::warn!("clipboard paste error: {err:?}"); + } + return; + } + + self.text = text + } +} diff --git a/tetanes/src/nes/renderer/event.rs b/tetanes/src/nes/renderer/event.rs new file mode 100644 index 00000000..c0210761 --- /dev/null +++ b/tetanes/src/nes/renderer/event.rs @@ -0,0 +1,1055 @@ +use crate::{ + feature, + nes::{ + config::Config, + event::{ConfigEvent, NesEvent, RendererEvent, Response, UiEvent}, + input::{Gamepads, Input}, + renderer::{ + gui::{lib::pixels_per_point, Gui}, + Renderer, State, Viewport, + }, + }, +}; +use egui::{PointerButton, SystemTheme, ViewportCommand, ViewportId}; +use winit::{ + dpi::PhysicalPosition, + event::{ + ElementState, Force, KeyEvent, MouseButton, MouseScrollDelta, Touch, TouchPhase, + WindowEvent, + }, + keyboard::{Key, KeyCode, ModifiersState, NamedKey, PhysicalKey}, + window::{Theme, WindowId}, +}; + +impl Renderer { + /// Handle event. + pub fn on_event(&mut self, event: &NesEvent, cfg: &Config) { + #[cfg(feature = "profiling")] + puffin::profile_function!(); + + self.gui.borrow_mut().on_event(event); + + match event { + NesEvent::Renderer(event) => match event { + RendererEvent::ViewportResized(_) => self.resize_window(cfg), + RendererEvent::ResizeTexture => self.resize_texture = true, + RendererEvent::RomLoaded(_) => { + let state = self.state.borrow(); + if state.focused != Some(ViewportId::ROOT) { + self.ctx + .send_viewport_cmd_to(ViewportId::ROOT, ViewportCommand::Focus); + } + } + _ => (), + }, + NesEvent::Config(event) => match event { + ConfigEvent::DarkTheme(enabled) => { + self.ctx.set_visuals(if *enabled { + Gui::dark_theme() + } else { + Gui::light_theme() + }); + } + ConfigEvent::EmbedViewports(embed) => { + if feature!(OsViewports) { + self.ctx.set_embed_viewports(*embed); + } + } + ConfigEvent::Fullscreen(fullscreen) => { + if feature!(OsViewports) { + self.ctx + .set_embed_viewports(*fullscreen || cfg.renderer.embed_viewports); + } + if self.fullscreen() != *fullscreen { + self.ctx + .send_viewport_cmd_to(ViewportId::ROOT, ViewportCommand::Focus); + self.ctx.send_viewport_cmd_to( + ViewportId::ROOT, + ViewportCommand::Fullscreen(*fullscreen), + ); + } + } + ConfigEvent::Region(_) | ConfigEvent::HideOverscan(_) | ConfigEvent::Scale(_) => { + self.resize_texture = true; + } + ConfigEvent::Shader(shader) => { + self.painter.borrow_mut().set_shader(*shader); + } + _ => (), + }, + // TODO: Update accesskit when egui supports an updated version + // #[cfg(not(target_arch = "wasm32"))] + // NesEvent::AccessKit { window_id, event } => { + // use crate::nes::event::AccessKitWindowEvent; + // if let Some(viewport_id) = self.viewport_id_for_window(*window_id) { + // let mut state = self.state.borrow_mut(); + // if let Some(viewport) = state.viewports.get_mut(&viewport_id) { + // match event { + // AccessKitWindowEvent::InitialTreeRequested => { + // self.ctx.enable_accesskit(); + // let update = self.ctx.accesskit_placeholder_tree_update(); + // self.accesskit.update_if_active(|| update); + // } + // AccessKitWindowEvent::ActionRequested(request) => { + // viewport + // .raw_input + // .events + // .push(egui::Event::AccessKitActionRequest(request.clone())); + // } + // AccessKitWindowEvent::AccessibilityDeactivated => { + // self.ctx.disable_accesskit(); + // } + // } + + // self.ctx.request_repaint_of(viewport_id); + // }; + // } + // } + _ => (), + } + } + + /// Handle window event. + pub fn on_window_event(&mut self, window_id: WindowId, event: &WindowEvent) -> Response { + #[cfg(feature = "profiling")] + puffin::profile_function!(); + + let Some(viewport_id) = self.viewport_id_for_window(window_id) else { + return Response::default(); + }; + + let State { + viewports, focused, .. + } = &mut *self.state.borrow_mut(); + let Some(viewport) = viewports.get_mut(&viewport_id) else { + return Response::default(); + }; + + #[cfg(not(target_arch = "wasm32"))] + if let Some(window) = &viewport.window { + tracing::trace!("process accesskit event: {event:?}"); + self.accesskit.process_event(window, event); + } + + let pixels_per_point = viewport + .window + .as_ref() + .map_or(1.0, |window| pixels_per_point(&self.ctx, window)); + + match event { + WindowEvent::Focused(new_focused) => { + *focused = if *new_focused { + Some(viewport_id) + } else { + None + }; + } + // Note: Does not trigger on all platforms + WindowEvent::Occluded(occluded) => viewport.occluded = *occluded, + WindowEvent::CloseRequested | WindowEvent::Destroyed => { + if viewport_id == ViewportId::ROOT { + self.tx.event(UiEvent::Terminate); + } else { + viewport.info.events.push(egui::ViewportEvent::Close); + + // We may need to repaint both us and our parent to close the window, + // and perhaps twice (once to notice the close-event, once again to enforce it). + // `request_repaint_of` does a double-repaint though: + self.ctx.request_repaint_of(viewport_id); + self.ctx.request_repaint_of(viewport.ids.parent); + } + } + // To support clipboard in wasm, we need to intercept the Paste event so that + // we don't try to use the clipboard fallback logic for paste. Associated + // behavior in the wasm platform layer handles setting the clipboard text. + WindowEvent::KeyboardInput { + event: + KeyEvent { + physical_key: PhysicalKey::Code(key), + .. + }, + .. + } => { + if let Some(key) = key_from_keycode(*key) { + use egui::Key; + + let modifiers = self.ctx.input(|i| i.modifiers); + + if feature!(ConsumePaste) && is_paste_command(modifiers, key) { + return Response { + consumed: true, + repaint: true, + }; + } + + if matches!(key, Key::Plus | Key::Equals | Key::Minus | Key::Num0) + && (modifiers.ctrl || modifiers.command) + { + self.zoom_changed = true; + } + } + } + WindowEvent::Resized(size) => { + self.painter + .borrow_mut() + .on_window_resized(viewport_id, size.width, size.height); + } + WindowEvent::ThemeChanged(theme) => { + self.ctx + .send_viewport_cmd(ViewportCommand::SetTheme(if *theme == Theme::Light { + SystemTheme::Light + } else { + SystemTheme::Dark + })); + } + _ => (), + }; + + let res = match event { + WindowEvent::ScaleFactorChanged { scale_factor, .. } => { + let native_pixels_per_point = *scale_factor as f32; + viewport.info.native_pixels_per_point = Some(native_pixels_per_point); + Response { + repaint: true, + consumed: false, + } + } + WindowEvent::MouseInput { state, button, .. } => { + Self::on_mouse_button_input(viewport.cursor_pos, viewport, *state, *button); + Response { + repaint: true, + consumed: self.ctx.wants_pointer_input(), + } + } + WindowEvent::MouseWheel { delta, .. } => { + Self::on_mouse_wheel(viewport, pixels_per_point, *delta); + Response { + repaint: true, + consumed: self.ctx.wants_pointer_input(), + } + } + WindowEvent::CursorMoved { position, .. } => { + Self::on_cursor_moved(viewport, pixels_per_point, *position); + Response { + repaint: true, + consumed: self.ctx.is_using_pointer(), + } + } + WindowEvent::CursorLeft { .. } => { + viewport.cursor_pos = None; + viewport.raw_input.events.push(egui::Event::PointerGone); + Response { + repaint: true, + consumed: false, + } + } + // WindowEvent::TouchpadPressure {device_id, pressure, stage, .. } => {} // TODO + WindowEvent::Touch(touch) => { + Self::on_touch(viewport, pixels_per_point, touch); + let consumed = match touch.phase { + TouchPhase::Started | TouchPhase::Ended | TouchPhase::Cancelled => { + self.ctx.wants_pointer_input() + } + TouchPhase::Moved => self.ctx.is_using_pointer(), + }; + Response { + repaint: true, + consumed, + } + } + WindowEvent::KeyboardInput { + event, + is_synthetic, + .. + } => { + // Winit generates fake "synthetic" KeyboardInput events when the focus + // is changed to the window, or away from it. Synthetic key presses + // represent no real key presses and should be ignored. + // See https://github.com/rust-windowing/winit/issues/3543 + if *is_synthetic && event.state == ElementState::Pressed { + Response { + repaint: true, + consumed: false, + } + } else { + Self::on_keyboard_input(viewport, event); + + // When pressing the Tab key, egui focuses the first focusable element, hence Tab always consumes. + let consumed = self.ctx.wants_keyboard_input() + || event.logical_key == Key::Named(NamedKey::Tab); + Response { + repaint: true, + consumed, + } + } + } + WindowEvent::Focused(focused) => { + viewport + .raw_input + .events + .push(egui::Event::WindowFocused(*focused)); + Response { + repaint: true, + consumed: false, + } + } + WindowEvent::HoveredFile(path) => { + if let Some(viewport) = viewports.get_mut(&viewport_id) { + viewport.raw_input.hovered_files.push(egui::HoveredFile { + path: Some(path.clone()), + ..Default::default() + }); + } + Response { + repaint: true, + consumed: false, + } + } + WindowEvent::HoveredFileCancelled => { + if let Some(viewport) = viewports.get_mut(&viewport_id) { + viewport.raw_input.hovered_files.clear(); + } + Response { + repaint: true, + consumed: false, + } + } + WindowEvent::DroppedFile(path) => { + if let Some(viewport) = viewports.get_mut(&viewport_id) { + viewport.raw_input.hovered_files.clear(); + viewport.raw_input.dropped_files.push(egui::DroppedFile { + path: Some(path.clone()), + ..Default::default() + }); + } + Response { + repaint: true, + consumed: false, + } + } + WindowEvent::ModifiersChanged(state) => { + let state = state.state(); + + let alt = state.alt_key(); + let ctrl = state.control_key(); + let shift = state.shift_key(); + let super_ = state.super_key(); + + if let Some(viewport) = viewports.get_mut(&viewport_id) { + viewport.raw_input.modifiers.alt = alt; + viewport.raw_input.modifiers.ctrl = ctrl; + viewport.raw_input.modifiers.shift = shift; + viewport.raw_input.modifiers.mac_cmd = cfg!(target_os = "macos") && super_; + viewport.raw_input.modifiers.command = if cfg!(target_os = "macos") { + super_ + } else { + ctrl + }; + } + + Response { + repaint: true, + consumed: false, + } + } + + // Things that may require repaint: + WindowEvent::RedrawRequested + | WindowEvent::CursorEntered { .. } + | WindowEvent::Destroyed + | WindowEvent::Occluded(_) + | WindowEvent::Resized(_) + | WindowEvent::Moved(_) + | WindowEvent::ThemeChanged(_) + | WindowEvent::TouchpadPressure { .. } + | WindowEvent::CloseRequested => Response { + repaint: true, + consumed: false, + }, + + // Things we completely ignore: + WindowEvent::ActivationTokenDone { .. } + | WindowEvent::AxisMotion { .. } + | WindowEvent::DoubleTapGesture { .. } + | WindowEvent::RotationGesture { .. } + | WindowEvent::PanGesture { .. } => Response { + repaint: false, + consumed: false, + }, + + WindowEvent::PinchGesture { delta, .. } => { + // Positive delta values indicate magnification (zooming in). + // Negative delta values indicate shrinking (zooming out). + let zoom_factor = (*delta as f32).exp(); + viewport + .raw_input + .events + .push(egui::Event::Zoom(zoom_factor)); + Response { + repaint: true, + consumed: self.ctx.wants_pointer_input(), + } + } + WindowEvent::Ime(_) => Response::default(), + }; + + let gui_res = self.gui.borrow_mut().on_window_event(event); + + Response { + repaint: res.repaint || gui_res.repaint, + consumed: res.consumed || gui_res.consumed, + } + } + + pub fn on_mouse_motion(&mut self, delta: (f64, f64)) { + let State { + viewports, focused, .. + } = &mut *self.state.borrow_mut(); + if let Some(id) = *focused { + if let Some(viewport) = viewports.get_mut(&id) { + viewport + .raw_input + .events + .push(egui::Event::MouseMoved(egui::Vec2 { + x: delta.0 as f32, + y: delta.1 as f32, + })); + } + } + } + + fn on_mouse_button_input( + pointer_pos: Option, + viewport: &mut Viewport, + state: ElementState, + button: MouseButton, + ) { + if let Some(pos) = pointer_pos { + if let Some(button) = pointer_button_from_mouse(button) { + let pressed = state == ElementState::Pressed; + + viewport.raw_input.events.push(egui::Event::PointerButton { + pos, + button, + pressed, + modifiers: viewport.raw_input.modifiers, + }); + } + } + } + + fn on_cursor_moved( + viewport: &mut Viewport, + pixels_per_point: f32, + pos_in_pixels: PhysicalPosition, + ) { + let pos_in_points = egui::pos2( + pos_in_pixels.x as f32 / pixels_per_point, + pos_in_pixels.y as f32 / pixels_per_point, + ); + viewport.cursor_pos = Some(pos_in_points); + + viewport + .raw_input + .events + .push(egui::Event::PointerMoved(pos_in_points)); + } + + fn on_touch(viewport: &mut Viewport, pixels_per_point: f32, touch: &Touch) { + // Emit touch event + viewport.raw_input.events.push(egui::Event::Touch { + device_id: egui::TouchDeviceId(egui::epaint::util::hash(touch.device_id)), + id: egui::TouchId::from(touch.id), + phase: match touch.phase { + TouchPhase::Started => egui::TouchPhase::Start, + TouchPhase::Moved => egui::TouchPhase::Move, + TouchPhase::Ended => egui::TouchPhase::End, + TouchPhase::Cancelled => egui::TouchPhase::Cancel, + }, + pos: egui::pos2( + touch.location.x as f32 / pixels_per_point, + touch.location.y as f32 / pixels_per_point, + ), + force: match touch.force { + Some(Force::Normalized(force)) => Some(force as f32), + Some(Force::Calibrated { + force, + max_possible_force, + .. + }) => Some((force / max_possible_force) as f32), + None => None, + }, + }); + } + + fn on_mouse_wheel(viewport: &mut Viewport, pixels_per_point: f32, delta: MouseScrollDelta) { + let modifiers = viewport.raw_input.modifiers; + let (unit, delta) = match delta { + MouseScrollDelta::LineDelta(x, y) => (egui::MouseWheelUnit::Line, egui::vec2(x, y)), + MouseScrollDelta::PixelDelta(PhysicalPosition { x, y }) => ( + egui::MouseWheelUnit::Point, + egui::vec2(x as f32, y as f32) / pixels_per_point, + ), + }; + viewport.raw_input.events.push(egui::Event::MouseWheel { + unit, + delta, + modifiers, + }); + } + + fn on_keyboard_input(viewport: &mut Viewport, event: &KeyEvent) { + let KeyEvent { + // Represents the position of a key independent of the currently active layout. + // + // It also uniquely identifies the physical key (i.e. it's mostly synonymous with a scancode). + // The most prevalent use case for this is games. For example the default keys for the player + // to move around might be the W, A, S, and D keys on a US layout. The position of these keys + // is more important than their label, so they should map to Z, Q, S, and D on an "AZERTY" + // layout. (This value is `KeyCode::KeyW` for the Z key on an AZERTY layout.) + physical_key, + // Represents the results of a keymap, i.e. what character a certain key press represents. + // When telling users "Press Ctrl-F to find", this is where we should + // look for the "F" key, because they may have a dvorak layout on + // a qwerty keyboard, and so the logical "F" character may not be located on the physical `KeyCode::KeyF` position. + logical_key, + text, + state, + .. + } = event; + + let pressed = *state == ElementState::Pressed; + + let physical_key = if let PhysicalKey::Code(keycode) = *physical_key { + key_from_keycode(keycode) + } else { + None + }; + + let logical_key = key_from_winit_key(logical_key); + + // Helpful logging to enable when adding new key support + tracing::trace!( + "logical {:?} -> {:?}, physical {:?} -> {:?}", + event.logical_key, + logical_key, + event.physical_key, + physical_key + ); + + let modifiers = viewport.raw_input.modifiers; + if let Some(logical_key) = logical_key { + if pressed { + if is_cut_command(modifiers, logical_key) { + viewport.raw_input.events.push(egui::Event::Cut); + return; + } else if is_copy_command(modifiers, logical_key) { + viewport.raw_input.events.push(egui::Event::Copy); + return; + } else if is_paste_command(modifiers, logical_key) { + if let Some(contents) = viewport.clipboard.get() { + let contents = contents.replace("\r\n", "\n"); + if !contents.is_empty() { + viewport.raw_input.events.push(egui::Event::Paste(contents)); + } + } + return; + } + } + + viewport.raw_input.events.push(egui::Event::Key { + key: logical_key, + physical_key, + pressed, + repeat: false, // egui will fill this in for us! + modifiers, + }); + } + + if let Some(text) = &text { + // Make sure there is text, and that it is not control characters + // (e.g. delete is sent as "\u{f728}" on macOS). + if !text.is_empty() && text.chars().all(is_printable_char) { + // On some platforms we get here when the user presses Cmd-C (copy), ctrl-W, etc. + // We need to ignore these characters that are side-effects of commands. + // Also make sure the key is pressed (not released). On Linux, text might + // contain some data even when the key is released. + let is_cmd = modifiers.ctrl || modifiers.command || modifiers.mac_cmd; + if pressed && !is_cmd { + viewport + .raw_input + .events + .push(egui::Event::Text(text.to_string())); + } + } + } + } + + /// Handle gamepad event updates. + pub fn on_gamepad_update(&self, gamepads: &Gamepads) -> Response { + #[cfg(feature = "profiling")] + puffin::profile_function!(); + + if self.gui.borrow().keybinds.wants_input() && gamepads.has_events() { + Response { + consumed: true, + repaint: true, + } + } else { + Response::default() + } + } +} + +impl TryFrom<(egui::Key, egui::Modifiers)> for Input { + type Error = (); + + fn try_from((key, modifiers): (egui::Key, egui::Modifiers)) -> Result { + let keycode = keycode_from_key(key).ok_or(())?; + let modifiers = modifiers_state_from_modifiers(modifiers); + Ok(Input::Key(keycode, modifiers)) + } +} + +impl From for Input { + fn from(button: PointerButton) -> Self { + Input::Mouse(mouse_button_from_pointer(button)) + } +} + +pub fn is_cut_command(modifiers: egui::Modifiers, keycode: egui::Key) -> bool { + keycode == egui::Key::Cut + || (modifiers.command && keycode == egui::Key::X) + || (cfg!(target_os = "windows") && modifiers.shift && keycode == egui::Key::Delete) +} + +pub fn is_copy_command(modifiers: egui::Modifiers, keycode: egui::Key) -> bool { + keycode == egui::Key::Copy + || (modifiers.command && keycode == egui::Key::C) + || (cfg!(target_os = "windows") && modifiers.ctrl && keycode == egui::Key::Insert) +} + +pub fn is_paste_command(modifiers: egui::Modifiers, keycode: egui::Key) -> bool { + keycode == egui::Key::Paste + || (modifiers.command && keycode == egui::Key::V) + || (cfg!(target_os = "windows") && modifiers.shift && keycode == egui::Key::Insert) +} + +/// Winit sends special keys (backspace, delete, F1, …) as characters. +/// Ignore those. +/// We also ignore '\r', '\n', '\t'. +/// Newlines are handled by the `Key::Enter` event. +pub const fn is_printable_char(chr: char) -> bool { + let is_in_private_use_area = '\u{e000}' <= chr && chr <= '\u{f8ff}' + || '\u{f0000}' <= chr && chr <= '\u{ffffd}' + || '\u{100000}' <= chr && chr <= '\u{10fffd}'; + + !is_in_private_use_area && !chr.is_ascii_control() +} + +pub fn key_from_winit_key(key: &winit::keyboard::Key) -> Option { + match key { + winit::keyboard::Key::Named(named_key) => key_from_named_key(*named_key), + winit::keyboard::Key::Character(str) => egui::Key::from_name(str.as_str()), + winit::keyboard::Key::Unidentified(_) | winit::keyboard::Key::Dead(_) => None, + } +} + +pub fn key_from_named_key(named_key: winit::keyboard::NamedKey) -> Option { + use egui::Key; + use winit::keyboard::NamedKey; + + Some(match named_key { + NamedKey::Enter => Key::Enter, + NamedKey::Tab => Key::Tab, + NamedKey::ArrowDown => Key::ArrowDown, + NamedKey::ArrowLeft => Key::ArrowLeft, + NamedKey::ArrowRight => Key::ArrowRight, + NamedKey::ArrowUp => Key::ArrowUp, + NamedKey::End => Key::End, + NamedKey::Home => Key::Home, + NamedKey::PageDown => Key::PageDown, + NamedKey::PageUp => Key::PageUp, + NamedKey::Backspace => Key::Backspace, + NamedKey::Delete => Key::Delete, + NamedKey::Insert => Key::Insert, + NamedKey::Escape => Key::Escape, + NamedKey::Cut => Key::Cut, + NamedKey::Copy => Key::Copy, + NamedKey::Paste => Key::Paste, + + NamedKey::Space => Key::Space, + + NamedKey::F1 => Key::F1, + NamedKey::F2 => Key::F2, + NamedKey::F3 => Key::F3, + NamedKey::F4 => Key::F4, + NamedKey::F5 => Key::F5, + NamedKey::F6 => Key::F6, + NamedKey::F7 => Key::F7, + NamedKey::F8 => Key::F8, + NamedKey::F9 => Key::F9, + NamedKey::F10 => Key::F10, + NamedKey::F11 => Key::F11, + NamedKey::F12 => Key::F12, + NamedKey::F13 => Key::F13, + NamedKey::F14 => Key::F14, + NamedKey::F15 => Key::F15, + NamedKey::F16 => Key::F16, + NamedKey::F17 => Key::F17, + NamedKey::F18 => Key::F18, + NamedKey::F19 => Key::F19, + NamedKey::F20 => Key::F20, + NamedKey::F21 => Key::F21, + NamedKey::F22 => Key::F22, + NamedKey::F23 => Key::F23, + NamedKey::F24 => Key::F24, + NamedKey::F25 => Key::F25, + NamedKey::F26 => Key::F26, + NamedKey::F27 => Key::F27, + NamedKey::F28 => Key::F28, + NamedKey::F29 => Key::F29, + NamedKey::F30 => Key::F30, + NamedKey::F31 => Key::F31, + NamedKey::F32 => Key::F32, + NamedKey::F33 => Key::F33, + NamedKey::F34 => Key::F34, + NamedKey::F35 => Key::F35, + _ => { + tracing::trace!("Unknown key: {named_key:?}"); + return None; + } + }) +} + +pub const fn key_from_keycode(keycode: KeyCode) -> Option { + Some(match keycode { + KeyCode::ArrowDown => egui::Key::ArrowDown, + KeyCode::ArrowLeft => egui::Key::ArrowLeft, + KeyCode::ArrowRight => egui::Key::ArrowRight, + KeyCode::ArrowUp => egui::Key::ArrowUp, + + KeyCode::Escape => egui::Key::Escape, + KeyCode::Tab => egui::Key::Tab, + KeyCode::Backspace => egui::Key::Backspace, + KeyCode::Enter | KeyCode::NumpadEnter => egui::Key::Enter, + + KeyCode::Insert => egui::Key::Insert, + KeyCode::Delete => egui::Key::Delete, + KeyCode::Home => egui::Key::Home, + KeyCode::End => egui::Key::End, + KeyCode::PageUp => egui::Key::PageUp, + KeyCode::PageDown => egui::Key::PageDown, + + // Punctuation + KeyCode::Space => egui::Key::Space, + KeyCode::Comma => egui::Key::Comma, + KeyCode::Period => egui::Key::Period, + KeyCode::Semicolon => egui::Key::Semicolon, + KeyCode::Backslash => egui::Key::Backslash, + KeyCode::Slash | KeyCode::NumpadDivide => egui::Key::Slash, + KeyCode::BracketLeft => egui::Key::OpenBracket, + KeyCode::BracketRight => egui::Key::CloseBracket, + KeyCode::Backquote => egui::Key::Backtick, + + KeyCode::Cut => egui::Key::Cut, + KeyCode::Copy => egui::Key::Copy, + KeyCode::Paste => egui::Key::Paste, + KeyCode::Minus | KeyCode::NumpadSubtract => egui::Key::Minus, + KeyCode::NumpadAdd => egui::Key::Plus, + KeyCode::Equal => egui::Key::Equals, + + KeyCode::Digit0 | KeyCode::Numpad0 => egui::Key::Num0, + KeyCode::Digit1 | KeyCode::Numpad1 => egui::Key::Num1, + KeyCode::Digit2 | KeyCode::Numpad2 => egui::Key::Num2, + KeyCode::Digit3 | KeyCode::Numpad3 => egui::Key::Num3, + KeyCode::Digit4 | KeyCode::Numpad4 => egui::Key::Num4, + KeyCode::Digit5 | KeyCode::Numpad5 => egui::Key::Num5, + KeyCode::Digit6 | KeyCode::Numpad6 => egui::Key::Num6, + KeyCode::Digit7 | KeyCode::Numpad7 => egui::Key::Num7, + KeyCode::Digit8 | KeyCode::Numpad8 => egui::Key::Num8, + KeyCode::Digit9 | KeyCode::Numpad9 => egui::Key::Num9, + + KeyCode::KeyA => egui::Key::A, + KeyCode::KeyB => egui::Key::B, + KeyCode::KeyC => egui::Key::C, + KeyCode::KeyD => egui::Key::D, + KeyCode::KeyE => egui::Key::E, + KeyCode::KeyF => egui::Key::F, + KeyCode::KeyG => egui::Key::G, + KeyCode::KeyH => egui::Key::H, + KeyCode::KeyI => egui::Key::I, + KeyCode::KeyJ => egui::Key::J, + KeyCode::KeyK => egui::Key::K, + KeyCode::KeyL => egui::Key::L, + KeyCode::KeyM => egui::Key::M, + KeyCode::KeyN => egui::Key::N, + KeyCode::KeyO => egui::Key::O, + KeyCode::KeyP => egui::Key::P, + KeyCode::KeyQ => egui::Key::Q, + KeyCode::KeyR => egui::Key::R, + KeyCode::KeyS => egui::Key::S, + KeyCode::KeyT => egui::Key::T, + KeyCode::KeyU => egui::Key::U, + KeyCode::KeyV => egui::Key::V, + KeyCode::KeyW => egui::Key::W, + KeyCode::KeyX => egui::Key::X, + KeyCode::KeyY => egui::Key::Y, + KeyCode::KeyZ => egui::Key::Z, + + KeyCode::F1 => egui::Key::F1, + KeyCode::F2 => egui::Key::F2, + KeyCode::F3 => egui::Key::F3, + KeyCode::F4 => egui::Key::F4, + KeyCode::F5 => egui::Key::F5, + KeyCode::F6 => egui::Key::F6, + KeyCode::F7 => egui::Key::F7, + KeyCode::F8 => egui::Key::F8, + KeyCode::F9 => egui::Key::F9, + KeyCode::F10 => egui::Key::F10, + KeyCode::F11 => egui::Key::F11, + KeyCode::F12 => egui::Key::F12, + KeyCode::F13 => egui::Key::F13, + KeyCode::F14 => egui::Key::F14, + KeyCode::F15 => egui::Key::F15, + KeyCode::F16 => egui::Key::F16, + KeyCode::F17 => egui::Key::F17, + KeyCode::F18 => egui::Key::F18, + KeyCode::F19 => egui::Key::F19, + KeyCode::F20 => egui::Key::F20, + KeyCode::F21 => egui::Key::F21, + KeyCode::F22 => egui::Key::F22, + KeyCode::F23 => egui::Key::F23, + KeyCode::F24 => egui::Key::F24, + KeyCode::F25 => egui::Key::F25, + KeyCode::F26 => egui::Key::F26, + KeyCode::F27 => egui::Key::F27, + KeyCode::F28 => egui::Key::F28, + KeyCode::F29 => egui::Key::F29, + KeyCode::F30 => egui::Key::F30, + KeyCode::F31 => egui::Key::F31, + KeyCode::F32 => egui::Key::F32, + KeyCode::F33 => egui::Key::F33, + KeyCode::F34 => egui::Key::F34, + KeyCode::F35 => egui::Key::F35, + _ => { + return None; + } + }) +} + +pub const fn keycode_from_key(key: egui::Key) -> Option { + Some(match key { + egui::Key::ArrowDown => KeyCode::ArrowDown, + egui::Key::ArrowLeft => KeyCode::ArrowLeft, + egui::Key::ArrowRight => KeyCode::ArrowRight, + egui::Key::ArrowUp => KeyCode::ArrowUp, + + egui::Key::Escape => KeyCode::Escape, + egui::Key::Tab => KeyCode::Tab, + egui::Key::Backspace => KeyCode::Backspace, + egui::Key::Enter => KeyCode::Enter, + + egui::Key::Insert => KeyCode::Insert, + egui::Key::Delete => KeyCode::Delete, + egui::Key::Home => KeyCode::Home, + egui::Key::End => KeyCode::End, + egui::Key::PageUp => KeyCode::PageUp, + egui::Key::PageDown => KeyCode::PageDown, + + // Punctuation + egui::Key::Space => KeyCode::Space, + egui::Key::Comma => KeyCode::Comma, + egui::Key::Period => KeyCode::Period, + egui::Key::Semicolon => KeyCode::Semicolon, + egui::Key::Backslash => KeyCode::Backslash, + egui::Key::Slash => KeyCode::Slash, + egui::Key::OpenBracket => KeyCode::BracketLeft, + egui::Key::CloseBracket => KeyCode::BracketRight, + + egui::Key::Cut => KeyCode::Cut, + egui::Key::Copy => KeyCode::Copy, + egui::Key::Paste => KeyCode::Paste, + egui::Key::Minus => KeyCode::Minus, + egui::Key::Plus => KeyCode::NumpadAdd, + egui::Key::Equals => KeyCode::Equal, + + egui::Key::Num0 => KeyCode::Digit0, + egui::Key::Num1 => KeyCode::Digit1, + egui::Key::Num2 => KeyCode::Digit2, + egui::Key::Num3 => KeyCode::Digit3, + egui::Key::Num4 => KeyCode::Digit4, + egui::Key::Num5 => KeyCode::Digit5, + egui::Key::Num6 => KeyCode::Digit6, + egui::Key::Num7 => KeyCode::Digit7, + egui::Key::Num8 => KeyCode::Digit8, + egui::Key::Num9 => KeyCode::Digit9, + + egui::Key::A => KeyCode::KeyA, + egui::Key::B => KeyCode::KeyB, + egui::Key::C => KeyCode::KeyC, + egui::Key::D => KeyCode::KeyD, + egui::Key::E => KeyCode::KeyE, + egui::Key::F => KeyCode::KeyF, + egui::Key::G => KeyCode::KeyG, + egui::Key::H => KeyCode::KeyH, + egui::Key::I => KeyCode::KeyI, + egui::Key::J => KeyCode::KeyJ, + egui::Key::K => KeyCode::KeyK, + egui::Key::L => KeyCode::KeyL, + egui::Key::M => KeyCode::KeyM, + egui::Key::N => KeyCode::KeyN, + egui::Key::O => KeyCode::KeyO, + egui::Key::P => KeyCode::KeyP, + egui::Key::Q => KeyCode::KeyQ, + egui::Key::R => KeyCode::KeyR, + egui::Key::S => KeyCode::KeyS, + egui::Key::T => KeyCode::KeyT, + egui::Key::U => KeyCode::KeyU, + egui::Key::V => KeyCode::KeyV, + egui::Key::W => KeyCode::KeyW, + egui::Key::X => KeyCode::KeyX, + egui::Key::Y => KeyCode::KeyY, + egui::Key::Z => KeyCode::KeyZ, + + egui::Key::F1 => KeyCode::F1, + egui::Key::F2 => KeyCode::F2, + egui::Key::F3 => KeyCode::F3, + egui::Key::F4 => KeyCode::F4, + egui::Key::F5 => KeyCode::F5, + egui::Key::F6 => KeyCode::F6, + egui::Key::F7 => KeyCode::F7, + egui::Key::F8 => KeyCode::F8, + egui::Key::F9 => KeyCode::F9, + egui::Key::F10 => KeyCode::F10, + egui::Key::F11 => KeyCode::F11, + egui::Key::F12 => KeyCode::F12, + egui::Key::F13 => KeyCode::F13, + egui::Key::F14 => KeyCode::F14, + egui::Key::F15 => KeyCode::F15, + egui::Key::F16 => KeyCode::F16, + egui::Key::F17 => KeyCode::F17, + egui::Key::F18 => KeyCode::F18, + egui::Key::F19 => KeyCode::F19, + egui::Key::F20 => KeyCode::F20, + egui::Key::F21 => KeyCode::F21, + egui::Key::F22 => KeyCode::F22, + egui::Key::F23 => KeyCode::F23, + egui::Key::F24 => KeyCode::F24, + egui::Key::F25 => KeyCode::F25, + egui::Key::F26 => KeyCode::F26, + egui::Key::F27 => KeyCode::F27, + egui::Key::F28 => KeyCode::F28, + egui::Key::F29 => KeyCode::F29, + egui::Key::F30 => KeyCode::F30, + egui::Key::F31 => KeyCode::F31, + egui::Key::F32 => KeyCode::F32, + egui::Key::F33 => KeyCode::F33, + egui::Key::F34 => KeyCode::F34, + egui::Key::F35 => KeyCode::F35, + + _ => return None, + }) +} + +pub fn modifiers_from_modifiers_state(modifier_state: ModifiersState) -> egui::Modifiers { + egui::Modifiers { + alt: modifier_state.alt_key(), + ctrl: modifier_state.control_key(), + shift: modifier_state.shift_key(), + #[cfg(target_os = "macos")] + mac_cmd: modifier_state.super_key(), + #[cfg(not(target_os = "macos"))] + mac_cmd: false, + #[cfg(target_os = "macos")] + command: modifier_state.super_key(), + #[cfg(not(target_os = "macos"))] + command: modifier_state.control_key(), + } +} + +pub fn modifiers_state_from_modifiers(modifiers: egui::Modifiers) -> ModifiersState { + let mut modifiers_state = ModifiersState::empty(); + if modifiers.shift { + modifiers_state |= ModifiersState::SHIFT; + } + if modifiers.ctrl { + modifiers_state |= ModifiersState::CONTROL; + } + if modifiers.alt { + modifiers_state |= ModifiersState::ALT; + } + #[cfg(target_os = "macos")] + if modifiers.mac_cmd { + modifiers_state |= ModifiersState::SUPER; + } + // TODO: egui doesn't seem to support SUPER on Windows/Linux + modifiers_state +} + +pub const fn pointer_button_from_mouse(button: MouseButton) -> Option { + Some(match button { + MouseButton::Left => PointerButton::Primary, + MouseButton::Right => PointerButton::Secondary, + MouseButton::Middle => PointerButton::Middle, + MouseButton::Back => PointerButton::Extra1, + MouseButton::Forward => PointerButton::Extra2, + MouseButton::Other(_) => return None, + }) +} + +pub const fn mouse_button_from_pointer(button: PointerButton) -> MouseButton { + match button { + PointerButton::Primary => MouseButton::Left, + PointerButton::Secondary => MouseButton::Right, + PointerButton::Middle => MouseButton::Middle, + PointerButton::Extra1 => MouseButton::Back, + PointerButton::Extra2 => MouseButton::Forward, + } +} + +pub const fn translate_cursor(cursor_icon: egui::CursorIcon) -> Option { + use egui::CursorIcon; + + match cursor_icon { + CursorIcon::None => None, + + CursorIcon::Alias => Some(winit::window::CursorIcon::Alias), + CursorIcon::AllScroll => Some(winit::window::CursorIcon::AllScroll), + CursorIcon::Cell => Some(winit::window::CursorIcon::Cell), + CursorIcon::ContextMenu => Some(winit::window::CursorIcon::ContextMenu), + CursorIcon::Copy => Some(winit::window::CursorIcon::Copy), + CursorIcon::Crosshair => Some(winit::window::CursorIcon::Crosshair), + CursorIcon::Default => Some(winit::window::CursorIcon::Default), + CursorIcon::Grab => Some(winit::window::CursorIcon::Grab), + CursorIcon::Grabbing => Some(winit::window::CursorIcon::Grabbing), + CursorIcon::Help => Some(winit::window::CursorIcon::Help), + CursorIcon::Move => Some(winit::window::CursorIcon::Move), + CursorIcon::NoDrop => Some(winit::window::CursorIcon::NoDrop), + CursorIcon::NotAllowed => Some(winit::window::CursorIcon::NotAllowed), + CursorIcon::PointingHand => Some(winit::window::CursorIcon::Pointer), + CursorIcon::Progress => Some(winit::window::CursorIcon::Progress), + + CursorIcon::ResizeHorizontal => Some(winit::window::CursorIcon::EwResize), + CursorIcon::ResizeNeSw => Some(winit::window::CursorIcon::NeswResize), + CursorIcon::ResizeNwSe => Some(winit::window::CursorIcon::NwseResize), + CursorIcon::ResizeVertical => Some(winit::window::CursorIcon::NsResize), + + CursorIcon::ResizeEast => Some(winit::window::CursorIcon::EResize), + CursorIcon::ResizeSouthEast => Some(winit::window::CursorIcon::SeResize), + CursorIcon::ResizeSouth => Some(winit::window::CursorIcon::SResize), + CursorIcon::ResizeSouthWest => Some(winit::window::CursorIcon::SwResize), + CursorIcon::ResizeWest => Some(winit::window::CursorIcon::WResize), + CursorIcon::ResizeNorthWest => Some(winit::window::CursorIcon::NwResize), + CursorIcon::ResizeNorth => Some(winit::window::CursorIcon::NResize), + CursorIcon::ResizeNorthEast => Some(winit::window::CursorIcon::NeResize), + CursorIcon::ResizeColumn => Some(winit::window::CursorIcon::ColResize), + CursorIcon::ResizeRow => Some(winit::window::CursorIcon::RowResize), + + CursorIcon::Text => Some(winit::window::CursorIcon::Text), + CursorIcon::VerticalText => Some(winit::window::CursorIcon::VerticalText), + CursorIcon::Wait => Some(winit::window::CursorIcon::Wait), + CursorIcon::ZoomIn => Some(winit::window::CursorIcon::ZoomIn), + CursorIcon::ZoomOut => Some(winit::window::CursorIcon::ZoomOut), + } +} diff --git a/tetanes/src/nes/renderer/gui.rs b/tetanes/src/nes/renderer/gui.rs index 891793da..86f879e6 100644 --- a/tetanes/src/nes/renderer/gui.rs +++ b/tetanes/src/nes/renderer/gui.rs @@ -5,20 +5,18 @@ use crate::{ config::{Config, RendererConfig}, emulation::FrameStats, event::{ - ConfigEvent, EmulationEvent, NesEvent, NesEventProxy, RendererEvent, RunState, UiEvent, + ConfigEvent, EmulationEvent, NesEvent, NesEventProxy, RendererEvent, Response, + RunState, UiEvent, }, input::Gamepads, - renderer::{ - gui::{ - keybinds::Keybinds, - lib::{ - cursor_to_zapper, input_down, ShortcutText, ShowShortcut, ToggleValue, - ViewportOptions, - }, - ppu_viewer::PpuViewer, - preferences::Preferences, + renderer::gui::{ + keybinds::Keybinds, + lib::{ + cursor_to_zapper, input_down, ShortcutText, ShowShortcut, ToggleValue, + ViewportOptions, }, - shader::{self, Shader}, + ppu_viewer::PpuViewer, + preferences::Preferences, }, rom::{RomAsset, HOMEBREW_ROMS}, version::Version, @@ -26,15 +24,14 @@ use crate::{ sys::{info::System, SystemInfo}, }; use egui::{ - include_image, + hex_color, include_image, load::SizedTexture, menu, - style::{HandleShape, Selection, WidgetVisuals}, + style::{HandleShape, Selection, TextCursorStyle, WidgetVisuals}, Align, Area, Button, CentralPanel, Color32, Context, CursorIcon, Direction, FontData, FontDefinitions, FontFamily, Frame, Grid, Id, Image, Layout, Order, Pos2, Rect, RichText, Rounding, ScrollArea, Sense, Stroke, TopBottomPanel, Ui, Vec2, Visuals, }; -use egui_winit::EventResponse; use serde::{Deserialize, Serialize}; use tetanes_core::{ action::Action as DeckAction, @@ -104,14 +101,6 @@ pub struct Gui { pub error: Option, } -// TODO: Remove once https://github.com/emilk/egui/pull/4372 is released -macro_rules! hex_color { - ($s:literal) => {{ - let array = color_hex::color_from_hex!($s); - Color32::from_rgb(array[0], array[1], array[2]) - }}; -} - impl Gui { const MSG_TIMEOUT: Duration = Duration::from_secs(3); const MAX_MESSAGES: usize = 5; @@ -156,7 +145,7 @@ impl Gui { } } - pub fn on_window_event(&mut self, event: &WindowEvent) -> EventResponse { + pub fn on_window_event(&mut self, event: &WindowEvent) -> Response { #[cfg(feature = "profiling")] puffin::profile_function!(); @@ -166,12 +155,12 @@ impl Gui { WindowEvent::KeyboardInput { .. } | WindowEvent::MouseInput { .. } ) { - EventResponse { + Response { consumed: true, ..Default::default() } } else { - EventResponse::default() + Response::default() } } @@ -323,11 +312,12 @@ impl Gui { .show(ctx, |ui| ctx.memory_ui(ui)); } - #[cfg(feature = "profiling")] - if viewport_opts.enabled { - puffin::profile_scope!("puffin"); - puffin_egui::show_viewport_if_enabled(ctx); - } + // TODO: Enable once updated to egui 0.28.0 + // #[cfg(feature = "profiling")] + // if viewport_opts.enabled { + // puffin::profile_scope!("puffin"); + // puffin_egui::show_viewport_if_enabled(ctx); + // } } fn initialize(&mut self, ctx: &Context) { @@ -514,34 +504,34 @@ impl Gui { #[cfg(feature = "profiling")] puffin::profile_function!(); - ui.set_enabled(!self.keybinds.wants_input()); + ui.add_enabled_ui(!self.keybinds.wants_input(), |ui| { + let inner_res = menu::bar(ui, |ui| { + ui.horizontal_wrapped(|ui| { + Self::toggle_dark_mode_button(&self.tx, ui); - let inner_res = menu::bar(ui, |ui| { - ui.horizontal_wrapped(|ui| { - Self::toggle_dark_mode_button(&self.tx, ui); - - ui.separator(); + ui.separator(); - ui.menu_button("📁 File", |ui| self.file_menu(ui)); - ui.menu_button("🔨 Controls", |ui| self.controls_menu(ui)); - ui.menu_button("🔧 Config", |ui| self.config_menu(ui)); - // icon: screen - ui.menu_button("🖵 Window", |ui| self.window_menu(ui)); - ui.menu_button("🕷 Debug", |ui| self.debug_menu(ui)); - ui.menu_button("❓ Help", |ui| self.help_menu(ui)); + ui.menu_button("📁 File", |ui| self.file_menu(ui)); + ui.menu_button("🔨 Controls", |ui| self.controls_menu(ui)); + ui.menu_button("🔧 Config", |ui| self.config_menu(ui)); + // icon: screen + ui.menu_button("🖵 Window", |ui| self.window_menu(ui)); + ui.menu_button("🕷 Debug", |ui| self.debug_menu(ui)); + ui.menu_button("❓ Help", |ui| self.help_menu(ui)); - ui.with_layout(Layout::right_to_left(Align::Center), |ui| { - egui::warn_if_debug_build(ui); + ui.with_layout(Layout::right_to_left(Align::Center), |ui| { + egui::warn_if_debug_build(ui); + }); }); }); + let spacing = ui.style().spacing.item_spacing; + let border = 1.0; + let height = inner_res.response.rect.height() + spacing.y + border; + if height != self.menu_height { + self.menu_height = height; + self.tx.event(RendererEvent::ResizeTexture); + } }); - let spacing = ui.style().spacing.item_spacing; - let border = 1.0; - let height = inner_res.response.rect.height() + spacing.y + border; - if height != self.menu_height { - self.menu_height = height; - self.tx.event(RendererEvent::ResizeTexture); - } } pub fn toggle_dark_mode_button(tx: &NesEventProxy, ui: &mut Ui) { @@ -673,7 +663,7 @@ impl Gui { }); } - if feature!(Viewports) { + if feature!(OsViewports) { ui.separator(); let button = Button::new("⎆ Quit").shortcut_text(cfg.shortcut(UiAction::Quit)); @@ -1169,26 +1159,7 @@ impl Gui { CursorIcon::Default }; - let res = if matches!(self.cfg.renderer.shader, Shader::None) { - ui.add(image) - } else { - let texture_load_res = - image.load_for_size(ui.ctx(), ui.available_size()); - let image_size = - texture_load_res.as_ref().ok().and_then(|t| t.size()); - let ui_size = image.calc_size(ui.available_size(), image_size); - let (rect, res) = ui.allocate_exact_size(ui_size, image_sense); - let res = res.on_hover_cursor(hover_cursor); - - if ui.is_rect_visible(rect) { - ui.painter().add(egui_wgpu::Callback::new_paint_callback( - rect, - shader::Renderer::new(rect), - )); - } - - res - }; + let res = ui.add(image).on_hover_cursor(hover_cursor); self.nes_frame = res.rect; if self.cfg.deck.zapper { @@ -1608,7 +1579,10 @@ impl Gui { window_highlight_topmost: true, menu_rounding: Rounding::ZERO, panel_fill: hex_color!("#14191f"), - text_cursor: Stroke::new(2.0, hex_color!("#95e6cb")), + text_cursor: TextCursorStyle { + stroke: Stroke::new(2.0, hex_color!("#95e6cb")), + ..Default::default() + }, striped: true, handle_shape: HandleShape::Rect { aspect_ratio: 1.25 }, ..Default::default() @@ -1673,7 +1647,10 @@ impl Gui { window_fill: hex_color!("#f0eee4"), window_stroke: Stroke::new(1.0, hex_color!("#d9d8d7")), panel_fill: hex_color!("#f0eee4"), - text_cursor: Stroke::new(2.0, hex_color!("#4cbf99")), + text_cursor: TextCursorStyle { + stroke: Stroke::new(2.0, hex_color!("#4cbf99")), + ..Default::default() + }, ..Self::dark_theme() } } diff --git a/tetanes/src/nes/renderer/gui/keybinds.rs b/tetanes/src/nes/renderer/gui/keybinds.rs index c0e1af06..9d8c2b00 100644 --- a/tetanes/src/nes/renderer/gui/keybinds.rs +++ b/tetanes/src/nes/renderer/gui/keybinds.rs @@ -1,12 +1,9 @@ -use crate::{ - feature, - nes::{ - action::Action, - config::Config, - event::{ConfigEvent, NesEventProxy}, - input::{Gamepads, Input}, - renderer::gui::lib::ViewportOptions, - }, +use crate::nes::{ + action::Action, + config::Config, + event::{ConfigEvent, NesEventProxy}, + input::{Gamepads, Input}, + renderer::gui::lib::ViewportOptions, }; use egui::{Align2, Button, CentralPanel, Context, Grid, ScrollArea, Ui, Vec2, ViewportClass}; use parking_lot::Mutex; @@ -134,11 +131,6 @@ impl Keybinds { #[cfg(feature = "profiling")] puffin::profile_function!(); - let mut viewport_builder = egui::ViewportBuilder::default().with_title(Self::TITLE); - if opts.always_on_top { - viewport_builder = viewport_builder.with_always_on_top(); - } - let open = Arc::clone(&self.open); let state = Arc::clone(&self.state); let Some((cfg, gamepad_state)) = self.resources.take() else { @@ -147,27 +139,24 @@ impl Keybinds { }; let viewport_id = egui::ViewportId::from_hash_of("keybinds"); - fn viewport_cb( - ctx: &Context, - class: ViewportClass, - open: &Arc, - enabled: bool, - state: &Arc>, - cfg: &Config, - gamepad_state: &GamepadState, - ) { + let mut viewport_builder = egui::ViewportBuilder::default().with_title(Self::TITLE); + if opts.always_on_top { + viewport_builder = viewport_builder.with_always_on_top(); + } + + ctx.show_viewport_deferred(viewport_id, viewport_builder, move |ctx, class| { if class == ViewportClass::Embedded { let mut window_open = open.load(Ordering::Acquire); egui::Window::new(Keybinds::TITLE) .open(&mut window_open) .default_rect(ctx.available_rect().shrink(16.0)) .show(ctx, |ui| { - state.lock().ui(ui, enabled, cfg, gamepad_state); + state.lock().ui(ui, opts.enabled, &cfg, &gamepad_state); }); open.store(window_open, Ordering::Release); } else { CentralPanel::default().show(ctx, |ui| { - state.lock().ui(ui, enabled, cfg, gamepad_state); + state.lock().ui(ui, opts.enabled, &cfg, &gamepad_state); }); if ctx.input(|i| i.viewport().close_requested()) { open.store(false, Ordering::Release); @@ -178,33 +167,7 @@ impl Keybinds { state.pending_input = None; state.gamepad_unassign_confirm = None; } - } - - if feature!(DeferredViewport) { - ctx.show_viewport_deferred(viewport_id, viewport_builder, move |ctx, class| { - viewport_cb( - ctx, - class, - &open, - opts.enabled, - &state, - &cfg, - &gamepad_state, - ); - }); - } else { - ctx.show_viewport_immediate(viewport_id, viewport_builder, move |ctx, class| { - viewport_cb( - ctx, - class, - &open, - opts.enabled, - &state, - &cfg, - &gamepad_state, - ); - }); - } + }); } } @@ -315,10 +278,11 @@ impl State { match connected_gamepads { Some(gamepads) => { if gamepads.is_empty() { - ui.set_enabled(false); - let combo = egui::ComboBox::from_id_source("assigned_gamepad") - .selected_text("No Gamepads Connected"); - combo.show_ui(ui, |_| {}); + ui.add_enabled_ui(false, |ui| { + let combo = egui::ComboBox::from_id_source("assigned_gamepad") + .selected_text("No Gamepads Connected"); + combo.show_ui(ui, |_| {}); + }); } else { let mut assigned = gamepads .iter() @@ -362,10 +326,11 @@ impl State { } } None => { - ui.set_enabled(false); - let combo = egui::ComboBox::from_id_source("assigned_gamepad") - .selected_text("Gamepads not supported"); - combo.show_ui(ui, |_| {}); + ui.add_enabled_ui(false, |ui| { + let combo = egui::ComboBox::from_id_source("assigned_gamepad") + .selected_text("Gamepads not supported"); + combo.show_ui(ui, |_| {}); + }); } } }); @@ -460,7 +425,7 @@ impl State { match *event { Event::Key { physical_key: Some(key), - pressed: true, + pressed: false, modifiers, .. } => { @@ -470,7 +435,7 @@ impl State { } Event::PointerButton { button, - pressed: true, + pressed: false, .. } => { return Some(Input::from(button)); diff --git a/tetanes/src/nes/renderer/gui/lib.rs b/tetanes/src/nes/renderer/gui/lib.rs index 19499fdb..a16d611b 100644 --- a/tetanes/src/nes/renderer/gui/lib.rs +++ b/tetanes/src/nes/renderer/gui/lib.rs @@ -1,17 +1,17 @@ use crate::nes::{ config::Config, input::{Gamepads, Input}, + renderer::event::{ + key_from_keycode, modifiers_from_modifiers_state, pointer_button_from_mouse, + }, }; use egui::{ - Align, Checkbox, Context, Key, KeyboardShortcut, Layout, Modifiers, PointerButton, Pos2, Rect, - Response, RichText, Ui, Widget, WidgetText, + Align, Checkbox, Context, KeyboardShortcut, Layout, Pos2, Rect, Response, RichText, Ui, Widget, + WidgetText, }; use std::ops::{Deref, DerefMut}; use tetanes_core::ppu::Ppu; -use winit::{ - event::{ElementState, MouseButton}, - keyboard::{KeyCode, ModifiersState}, -}; +use winit::{event::ElementState, window::Window}; #[derive(Debug, Copy, Clone)] #[must_use] @@ -84,12 +84,6 @@ pub fn input_down(ui: &mut Ui, gamepads: Option<&Gamepads>, cfg: &Config, input: }) } -pub fn is_paste_command(modifiers: egui::Modifiers, keycode: Key) -> bool { - keycode == Key::Paste - || (modifiers.command && keycode == Key::V) - || (cfg!(target_os = "windows") && modifiers.shift && keycode == Key::Insert) -} - #[must_use] pub struct ShortcutWidget<'a, T> { inner: T, @@ -203,320 +197,66 @@ impl TryFrom for KeyboardShortcut { } } -impl TryFrom<(Key, Modifiers)> for Input { - type Error = (); - - fn try_from((key, modifiers): (Key, Modifiers)) -> Result { - let keycode = keycode_from_key(key).ok_or(())?; - let modifiers = modifiers_state_from_modifiers(modifiers); - Ok(Input::Key(keycode, modifiers)) - } -} - -impl From for Input { - fn from(button: PointerButton) -> Self { - Input::Mouse(mouse_button_from_pointer(button)) - } -} - -pub const fn key_from_keycode(keycode: KeyCode) -> Option { - Some(match keycode { - KeyCode::ArrowDown => Key::ArrowDown, - KeyCode::ArrowLeft => Key::ArrowLeft, - KeyCode::ArrowRight => Key::ArrowRight, - KeyCode::ArrowUp => Key::ArrowUp, - - KeyCode::Escape => Key::Escape, - KeyCode::Tab => Key::Tab, - KeyCode::Backspace => Key::Backspace, - KeyCode::Enter | KeyCode::NumpadEnter => Key::Enter, - - KeyCode::Insert => Key::Insert, - KeyCode::Delete => Key::Delete, - KeyCode::Home => Key::Home, - KeyCode::End => Key::End, - KeyCode::PageUp => Key::PageUp, - KeyCode::PageDown => Key::PageDown, - - // Punctuation - KeyCode::Space => Key::Space, - KeyCode::Comma => Key::Comma, - KeyCode::Period => Key::Period, - KeyCode::Semicolon => Key::Semicolon, - KeyCode::Backslash => Key::Backslash, - KeyCode::Slash | KeyCode::NumpadDivide => Key::Slash, - KeyCode::BracketLeft => Key::OpenBracket, - KeyCode::BracketRight => Key::CloseBracket, - KeyCode::Backquote => Key::Backtick, - - KeyCode::Cut => Key::Cut, - KeyCode::Copy => Key::Copy, - KeyCode::Paste => Key::Paste, - KeyCode::Minus | KeyCode::NumpadSubtract => Key::Minus, - KeyCode::NumpadAdd => Key::Plus, - KeyCode::Equal => Key::Equals, - - KeyCode::Digit0 | KeyCode::Numpad0 => Key::Num0, - KeyCode::Digit1 | KeyCode::Numpad1 => Key::Num1, - KeyCode::Digit2 | KeyCode::Numpad2 => Key::Num2, - KeyCode::Digit3 | KeyCode::Numpad3 => Key::Num3, - KeyCode::Digit4 | KeyCode::Numpad4 => Key::Num4, - KeyCode::Digit5 | KeyCode::Numpad5 => Key::Num5, - KeyCode::Digit6 | KeyCode::Numpad6 => Key::Num6, - KeyCode::Digit7 | KeyCode::Numpad7 => Key::Num7, - KeyCode::Digit8 | KeyCode::Numpad8 => Key::Num8, - KeyCode::Digit9 | KeyCode::Numpad9 => Key::Num9, - - KeyCode::KeyA => Key::A, - KeyCode::KeyB => Key::B, - KeyCode::KeyC => Key::C, - KeyCode::KeyD => Key::D, - KeyCode::KeyE => Key::E, - KeyCode::KeyF => Key::F, - KeyCode::KeyG => Key::G, - KeyCode::KeyH => Key::H, - KeyCode::KeyI => Key::I, - KeyCode::KeyJ => Key::J, - KeyCode::KeyK => Key::K, - KeyCode::KeyL => Key::L, - KeyCode::KeyM => Key::M, - KeyCode::KeyN => Key::N, - KeyCode::KeyO => Key::O, - KeyCode::KeyP => Key::P, - KeyCode::KeyQ => Key::Q, - KeyCode::KeyR => Key::R, - KeyCode::KeyS => Key::S, - KeyCode::KeyT => Key::T, - KeyCode::KeyU => Key::U, - KeyCode::KeyV => Key::V, - KeyCode::KeyW => Key::W, - KeyCode::KeyX => Key::X, - KeyCode::KeyY => Key::Y, - KeyCode::KeyZ => Key::Z, - - KeyCode::F1 => Key::F1, - KeyCode::F2 => Key::F2, - KeyCode::F3 => Key::F3, - KeyCode::F4 => Key::F4, - KeyCode::F5 => Key::F5, - KeyCode::F6 => Key::F6, - KeyCode::F7 => Key::F7, - KeyCode::F8 => Key::F8, - KeyCode::F9 => Key::F9, - KeyCode::F10 => Key::F10, - KeyCode::F11 => Key::F11, - KeyCode::F12 => Key::F12, - KeyCode::F13 => Key::F13, - KeyCode::F14 => Key::F14, - KeyCode::F15 => Key::F15, - KeyCode::F16 => Key::F16, - KeyCode::F17 => Key::F17, - KeyCode::F18 => Key::F18, - KeyCode::F19 => Key::F19, - KeyCode::F20 => Key::F20, - KeyCode::F21 => Key::F21, - KeyCode::F22 => Key::F22, - KeyCode::F23 => Key::F23, - KeyCode::F24 => Key::F24, - KeyCode::F25 => Key::F25, - KeyCode::F26 => Key::F26, - KeyCode::F27 => Key::F27, - KeyCode::F28 => Key::F28, - KeyCode::F29 => Key::F29, - KeyCode::F30 => Key::F30, - KeyCode::F31 => Key::F31, - KeyCode::F32 => Key::F32, - KeyCode::F33 => Key::F33, - KeyCode::F34 => Key::F34, - KeyCode::F35 => Key::F35, - - _ => { - return None; +pub fn screen_center(ctx: &Context) -> Option { + ctx.input(|i| { + let outer_rect = i.viewport().outer_rect?; + let size = outer_rect.size(); + let monitor_size = i.viewport().monitor_size?; + if 1.0 < monitor_size.x && 1.0 < monitor_size.y { + let x = (monitor_size.x - size.x) / 2.0; + let y = (monitor_size.y - size.y) / 2.0; + Some(Pos2::new(x, y)) + } else { + None } }) } -pub const fn keycode_from_key(key: Key) -> Option { - Some(match key { - Key::ArrowDown => KeyCode::ArrowDown, - Key::ArrowLeft => KeyCode::ArrowLeft, - Key::ArrowRight => KeyCode::ArrowRight, - Key::ArrowUp => KeyCode::ArrowUp, - - Key::Escape => KeyCode::Escape, - Key::Tab => KeyCode::Tab, - Key::Backspace => KeyCode::Backspace, - Key::Enter => KeyCode::Enter, - - Key::Insert => KeyCode::Insert, - Key::Delete => KeyCode::Delete, - Key::Home => KeyCode::Home, - Key::End => KeyCode::End, - Key::PageUp => KeyCode::PageUp, - Key::PageDown => KeyCode::PageDown, - - // Punctuation - Key::Space => KeyCode::Space, - Key::Comma => KeyCode::Comma, - Key::Period => KeyCode::Period, - Key::Semicolon => KeyCode::Semicolon, - Key::Backslash => KeyCode::Backslash, - Key::Slash => KeyCode::Slash, - Key::OpenBracket => KeyCode::BracketLeft, - Key::CloseBracket => KeyCode::BracketRight, +pub fn screen_size_in_pixels(window: &Window) -> egui::Vec2 { + let size = window.inner_size(); + egui::vec2(size.width as f32, size.height as f32) +} - Key::Cut => KeyCode::Cut, - Key::Copy => KeyCode::Copy, - Key::Paste => KeyCode::Paste, - Key::Minus => KeyCode::Minus, - Key::Plus => KeyCode::NumpadAdd, - Key::Equals => KeyCode::Equal, +pub fn pixels_per_point(egui_ctx: &egui::Context, window: &Window) -> f32 { + let native_pixels_per_point = window.scale_factor() as f32; + let egui_zoom_factor = egui_ctx.zoom_factor(); + egui_zoom_factor * native_pixels_per_point +} - Key::Num0 => KeyCode::Digit0, - Key::Num1 => KeyCode::Digit1, - Key::Num2 => KeyCode::Digit2, - Key::Num3 => KeyCode::Digit3, - Key::Num4 => KeyCode::Digit4, - Key::Num5 => KeyCode::Digit5, - Key::Num6 => KeyCode::Digit6, - Key::Num7 => KeyCode::Digit7, - Key::Num8 => KeyCode::Digit8, - Key::Num9 => KeyCode::Digit9, +pub fn inner_rect_in_points(window: &Window, pixels_per_point: f32) -> Option { + let inner_pos_px = window.inner_position().ok()?; + let inner_pos_px = egui::pos2(inner_pos_px.x as f32, inner_pos_px.y as f32); - Key::A => KeyCode::KeyA, - Key::B => KeyCode::KeyB, - Key::C => KeyCode::KeyC, - Key::D => KeyCode::KeyD, - Key::E => KeyCode::KeyE, - Key::F => KeyCode::KeyF, - Key::G => KeyCode::KeyG, - Key::H => KeyCode::KeyH, - Key::I => KeyCode::KeyI, - Key::J => KeyCode::KeyJ, - Key::K => KeyCode::KeyK, - Key::L => KeyCode::KeyL, - Key::M => KeyCode::KeyM, - Key::N => KeyCode::KeyN, - Key::O => KeyCode::KeyO, - Key::P => KeyCode::KeyP, - Key::Q => KeyCode::KeyQ, - Key::R => KeyCode::KeyR, - Key::S => KeyCode::KeyS, - Key::T => KeyCode::KeyT, - Key::U => KeyCode::KeyU, - Key::V => KeyCode::KeyV, - Key::W => KeyCode::KeyW, - Key::X => KeyCode::KeyX, - Key::Y => KeyCode::KeyY, - Key::Z => KeyCode::KeyZ, + let inner_size_px = window.inner_size(); + let inner_size_px = egui::vec2(inner_size_px.width as f32, inner_size_px.height as f32); - Key::F1 => KeyCode::F1, - Key::F2 => KeyCode::F2, - Key::F3 => KeyCode::F3, - Key::F4 => KeyCode::F4, - Key::F5 => KeyCode::F5, - Key::F6 => KeyCode::F6, - Key::F7 => KeyCode::F7, - Key::F8 => KeyCode::F8, - Key::F9 => KeyCode::F9, - Key::F10 => KeyCode::F10, - Key::F11 => KeyCode::F11, - Key::F12 => KeyCode::F12, - Key::F13 => KeyCode::F13, - Key::F14 => KeyCode::F14, - Key::F15 => KeyCode::F15, - Key::F16 => KeyCode::F16, - Key::F17 => KeyCode::F17, - Key::F18 => KeyCode::F18, - Key::F19 => KeyCode::F19, - Key::F20 => KeyCode::F20, - Key::F21 => KeyCode::F21, - Key::F22 => KeyCode::F22, - Key::F23 => KeyCode::F23, - Key::F24 => KeyCode::F24, - Key::F25 => KeyCode::F25, - Key::F26 => KeyCode::F26, - Key::F27 => KeyCode::F27, - Key::F28 => KeyCode::F28, - Key::F29 => KeyCode::F29, - Key::F30 => KeyCode::F30, - Key::F31 => KeyCode::F31, - Key::F32 => KeyCode::F32, - Key::F33 => KeyCode::F33, - Key::F34 => KeyCode::F34, - Key::F35 => KeyCode::F35, + let inner_rect_px = egui::Rect::from_min_size(inner_pos_px, inner_size_px); - _ => return None, - }) + Some(inner_rect_px / pixels_per_point) } -pub fn modifiers_from_modifiers_state(modifier_state: ModifiersState) -> Modifiers { - Modifiers { - alt: modifier_state.alt_key(), - ctrl: modifier_state.control_key(), - shift: modifier_state.shift_key(), - #[cfg(target_os = "macos")] - mac_cmd: modifier_state.super_key(), - #[cfg(not(target_os = "macos"))] - mac_cmd: false, - #[cfg(target_os = "macos")] - command: modifier_state.super_key(), - #[cfg(not(target_os = "macos"))] - command: modifier_state.control_key(), - } -} +pub fn outer_rect_in_points(window: &Window, pixels_per_point: f32) -> Option { + let outer_pos_px = window.outer_position().ok()?; + let outer_pos_px = egui::pos2(outer_pos_px.x as f32, outer_pos_px.y as f32); -pub fn modifiers_state_from_modifiers(modifiers: Modifiers) -> ModifiersState { - let mut modifiers_state = ModifiersState::empty(); - if modifiers.shift { - modifiers_state |= ModifiersState::SHIFT; - } - if modifiers.ctrl { - modifiers_state |= ModifiersState::CONTROL; - } - if modifiers.alt { - modifiers_state |= ModifiersState::ALT; - } - #[cfg(target_os = "macos")] - if modifiers.mac_cmd { - modifiers_state |= ModifiersState::SUPER; - } - // TODO: egui doesn't seem to support SUPER on Windows/Linux - modifiers_state -} + let outer_size_px = window.outer_size(); + let outer_size_px = egui::vec2(outer_size_px.width as f32, outer_size_px.height as f32); -pub const fn pointer_button_from_mouse(button: MouseButton) -> Option { - Some(match button { - MouseButton::Left => PointerButton::Primary, - MouseButton::Right => PointerButton::Secondary, - MouseButton::Middle => PointerButton::Middle, - MouseButton::Back => PointerButton::Extra1, - MouseButton::Forward => PointerButton::Extra2, - MouseButton::Other(_) => return None, - }) -} + let outer_rect_px = egui::Rect::from_min_size(outer_pos_px, outer_size_px); -pub const fn mouse_button_from_pointer(button: PointerButton) -> MouseButton { - match button { - PointerButton::Primary => MouseButton::Left, - PointerButton::Secondary => MouseButton::Right, - PointerButton::Middle => MouseButton::Middle, - PointerButton::Extra1 => MouseButton::Back, - PointerButton::Extra2 => MouseButton::Forward, - } + Some(outer_rect_px / pixels_per_point) } -pub fn screen_center(ctx: &Context) -> Option { - ctx.input(|i| { - let outer_rect = i.viewport().outer_rect?; - let size = outer_rect.size(); - let monitor_size = i.viewport().monitor_size?; - if 1.0 < monitor_size.x && 1.0 < monitor_size.y { - let x = (monitor_size.x - size.x) / 2.0; - let y = (monitor_size.y - size.y) / 2.0; - Some(Pos2::new(x, y)) - } else { - None +pub fn to_winit_icon(icon: &egui::IconData) -> Option { + if icon.is_empty() { + None + } else { + match winit::window::Icon::from_rgba(icon.rgba.clone(), icon.width, icon.height) { + Ok(winit_icon) => Some(winit_icon), + Err(err) => { + tracing::warn!("Invalid IconData: {err}"); + None + } } - }) + } } diff --git a/tetanes/src/nes/renderer/gui/ppu_viewer.rs b/tetanes/src/nes/renderer/gui/ppu_viewer.rs index 4668270e..f59b0221 100644 --- a/tetanes/src/nes/renderer/gui/ppu_viewer.rs +++ b/tetanes/src/nes/renderer/gui/ppu_viewer.rs @@ -1,7 +1,4 @@ -use crate::{ - feature, - nes::{config::Config, event::NesEventProxy, renderer::gui::lib::ViewportOptions}, -}; +use crate::nes::{config::Config, event::NesEventProxy, renderer::gui::lib::ViewportOptions}; use egui::{CentralPanel, Context, Ui, ViewportClass}; use parking_lot::Mutex; use std::sync::{ @@ -74,11 +71,6 @@ impl PpuViewer { #[cfg(feature = "profiling")] puffin::profile_function!(); - let mut viewport_builder = egui::ViewportBuilder::default().with_title(Self::TITLE); - if opts.always_on_top { - viewport_builder = viewport_builder.with_always_on_top(); - } - let open = Arc::clone(&self.open); let state = Arc::clone(&self.state); let Some(cfg) = self.resources.take() else { @@ -87,37 +79,25 @@ impl PpuViewer { }; let viewport_id = egui::ViewportId::from_hash_of("ppu_viewer"); - fn viewport_cb( - ctx: &Context, - class: ViewportClass, - open: &Arc, - enabled: bool, - state: &Arc>, - cfg: &Config, - ) { + let mut viewport_builder = egui::ViewportBuilder::default().with_title(Self::TITLE); + if opts.always_on_top { + viewport_builder = viewport_builder.with_always_on_top(); + } + + ctx.show_viewport_deferred(viewport_id, viewport_builder, move |ctx, class| { if class == ViewportClass::Embedded { let mut window_open = open.load(Ordering::Acquire); egui::Window::new(PpuViewer::TITLE) .open(&mut window_open) - .show(ctx, |ui| state.lock().ui(ui, enabled, cfg)); + .show(ctx, |ui| state.lock().ui(ui, opts.enabled, &cfg)); open.store(window_open, Ordering::Release); } else { - CentralPanel::default().show(ctx, |ui| state.lock().ui(ui, enabled, cfg)); + CentralPanel::default().show(ctx, |ui| state.lock().ui(ui, opts.enabled, &cfg)); if ctx.input(|i| i.viewport().close_requested()) { open.store(false, Ordering::Release); } } - } - - if feature!(DeferredViewport) { - ctx.show_viewport_deferred(viewport_id, viewport_builder, move |ctx, class| { - viewport_cb(ctx, class, &open, opts.enabled, &state, &cfg); - }); - } else { - ctx.show_viewport_immediate(viewport_id, viewport_builder, move |ctx, class| { - viewport_cb(ctx, class, &open, opts.enabled, &state, &cfg); - }); - } + }); } } diff --git a/tetanes/src/nes/renderer/gui/preferences.rs b/tetanes/src/nes/renderer/gui/preferences.rs index b6d3371d..2430369a 100644 --- a/tetanes/src/nes/renderer/gui/preferences.rs +++ b/tetanes/src/nes/renderer/gui/preferences.rs @@ -100,11 +100,6 @@ impl Preferences { #[cfg(feature = "profiling")] puffin::profile_function!(); - let mut viewport_builder = egui::ViewportBuilder::default().with_title(Self::TITLE); - if opts.always_on_top { - viewport_builder = viewport_builder.with_always_on_top(); - } - let open = Arc::clone(&self.open); let state = Arc::clone(&self.state); let Some(cfg) = self.resources.take() else { @@ -113,38 +108,26 @@ impl Preferences { }; let viewport_id = egui::ViewportId::from_hash_of("preferences"); - fn viewport_cb( - ctx: &Context, - class: ViewportClass, - open: &Arc, - enabled: bool, - state: &Arc>, - cfg: &Config, - ) { + let mut viewport_builder = egui::ViewportBuilder::default().with_title(Self::TITLE); + if opts.always_on_top { + viewport_builder = viewport_builder.with_always_on_top(); + } + + ctx.show_viewport_deferred(viewport_id, viewport_builder, move |ctx, class| { if class == ViewportClass::Embedded { let mut window_open = open.load(Ordering::Acquire); egui::Window::new(Preferences::TITLE) .open(&mut window_open) .default_rect(ctx.available_rect().shrink(16.0)) - .show(ctx, |ui| state.lock().ui(ui, enabled, cfg)); + .show(ctx, |ui| state.lock().ui(ui, opts.enabled, &cfg)); open.store(window_open, Ordering::Release); } else { - CentralPanel::default().show(ctx, |ui| state.lock().ui(ui, enabled, cfg)); + CentralPanel::default().show(ctx, |ui| state.lock().ui(ui, opts.enabled, &cfg)); if ctx.input(|i| i.viewport().close_requested()) { open.store(false, Ordering::Release); } } - } - - if feature!(DeferredViewport) { - ctx.show_viewport_deferred(viewport_id, viewport_builder, move |ctx, class| { - viewport_cb(ctx, class, &open, opts.enabled, &state, &cfg); - }); - } else { - ctx.show_viewport_immediate(viewport_id, viewport_builder, move |ctx, class| { - viewport_cb(ctx, class, &open, opts.enabled, &state, &cfg); - }); - } + }); } pub fn show_genie_codes_entry(&mut self, ui: &mut Ui, cfg: &Config) { @@ -467,7 +450,7 @@ impl Preferences { cfg: &Config, shortcut: impl Into>, ) { - if feature!(Viewports) { + if feature!(OsViewports) { ui.add_enabled_ui(!cfg.renderer.fullscreen, |ui| { let shortcut = shortcut.into(); // icon: maximize @@ -493,7 +476,7 @@ impl Preferences { mut always_on_top: bool, shortcut: impl Into>, ) { - if feature!(Viewports) { + if feature!(OsViewports) { let shortcut = shortcut.into(); let icon = shortcut.is_some().then_some("🔝 ").unwrap_or_default(); let checkbox = Checkbox::new(&mut always_on_top, format!("{icon}Always on Top")) @@ -609,7 +592,7 @@ impl State { ui.label("Seconds:") .on_hover_text("The maximum number of seconds to rewind."); let drag = DragValue::new(&mut rewind_seconds) - .clamp_range(1..=360) + .range(1..=360) .suffix(" seconds"); let res = ui.add(drag); if res.changed() { @@ -619,7 +602,7 @@ impl State { ui.label("Interval:") .on_hover_text("The frame interval to save rewind states."); let drag = DragValue::new(&mut rewind_interval) - .clamp_range(1..=60) + .range(1..=60) .suffix(" frames"); let res = ui.add(drag); if res.changed() { @@ -651,7 +634,7 @@ impl State { "A value of `0` will still save on exit or unload while Auto-Save is enabled." )); let drag = DragValue::new(&mut auto_save_interval) - .clamp_range(0..=60) + .range(0..=60) .suffix(" seconds"); let res = ui.add(drag); if res.changed() { @@ -804,7 +787,7 @@ impl State { ); let drag = DragValue::new(&mut buffer_size) .speed(10) - .clamp_range(0..=8192) + .range(0..=8192) .suffix(" samples"); let res = ui.add(drag); if res.changed() { @@ -819,7 +802,7 @@ impl State { ); let mut latency = latency.as_millis() as u64; let drag = DragValue::new(&mut latency) - .clamp_range(0..=1000) + .range(0..=1000) .suffix(" ms"); let res = ui.add(drag); if res.changed() { diff --git a/tetanes/src/nes/renderer/painter.rs b/tetanes/src/nes/renderer/painter.rs new file mode 100644 index 00000000..786a3e02 --- /dev/null +++ b/tetanes/src/nes/renderer/painter.rs @@ -0,0 +1,1032 @@ +use crate::nes::renderer::shader::{self, Shader}; +use anyhow::{anyhow, Context}; +use egui::{ + ahash::HashMap, + epaint::{self, Primitive, Vertex}, + NumExt, ViewportId, ViewportIdMap, ViewportIdSet, +}; +use std::{ + borrow::Cow, + collections::hash_map::Entry, + iter, + num::{NonZeroU32, NonZeroU64}, + ops::{Deref, Range}, + sync::Arc, +}; +use wgpu::util::DeviceExt; +use winit::{dpi::PhysicalSize, window::Window}; + +#[derive(Debug)] +#[must_use] +pub struct Surface { + inner: wgpu::Surface<'static>, + shader: Option, + width: u32, + height: u32, +} + +impl Surface { + pub fn new( + instance: &wgpu::Instance, + window: Arc, + size: PhysicalSize, + ) -> anyhow::Result { + Ok(Self { + inner: instance.create_surface(window)?, + shader: None, + width: size.width, + height: size.height, + }) + } + + fn create_texture_view( + &self, + device: &wgpu::Device, + format: wgpu::TextureFormat, + ) -> wgpu::TextureView { + device + .create_texture(&wgpu::TextureDescriptor { + label: Some("surface_texture"), + size: wgpu::Extent3d { + width: self.width, + height: self.height, + depth_or_array_layers: 1, + }, + mip_level_count: 1, + sample_count: 1, + dimension: wgpu::TextureDimension::D2, + format, + usage: wgpu::TextureUsages::RENDER_ATTACHMENT + | wgpu::TextureUsages::TEXTURE_BINDING, + view_formats: &[], + }) + .create_view(&wgpu::TextureViewDescriptor::default()) + } + + fn set_shader( + &mut self, + device: &wgpu::Device, + format: wgpu::TextureFormat, + uniform_bind_group_layout: &wgpu::BindGroupLayout, + shader: Shader, + ) { + if matches!(shader, Shader::None) { + self.shader = None; + } else { + self.shader = Some(shader::Resources::new( + device, + format, + self.create_texture_view(device, format), + uniform_bind_group_layout, + shader, + )); + } + } +} + +impl Deref for Surface { + type Target = wgpu::Surface<'static>; + fn deref(&self) -> &Self::Target { + &self.inner + } +} + +#[derive(Debug)] +#[must_use] +pub struct Painter { + instance: wgpu::Instance, + render_state: Option, + surfaces: ViewportIdMap, +} + +impl Default for Painter { + fn default() -> Self { + Self { + instance: wgpu::Instance::new(wgpu::InstanceDescriptor::default()), + render_state: None, + surfaces: Default::default(), + } + } +} + +impl Painter { + pub fn new() -> Self { + Self::default() + } + + pub fn set_shader(&mut self, shader: Shader) { + if let Some(render_state) = &mut self.render_state { + render_state.shader = shader; + if let Some((_, surface)) = self + .surfaces + .iter_mut() + .find(|(id, _)| **id == ViewportId::ROOT) + { + surface.set_shader( + &render_state.device, + render_state.format, + &render_state.uniform_bind_group_layout, + shader, + ); + } + } + } + + pub async fn set_window( + &mut self, + viewport_id: ViewportId, + window: Option>, + ) -> anyhow::Result<()> { + if let Some(window) = window { + if let Entry::Vacant(entry) = self.surfaces.entry(viewport_id) { + let size = window.inner_size(); + let mut surface = Surface::new(&self.instance, window, size)?; + + let render_state = match &mut self.render_state { + Some(render_state) => render_state, + None => { + let render_state = RenderState::create(&self.instance, &surface).await?; + self.render_state.get_or_insert(render_state) + } + }; + + if let (Some(width), Some(height)) = + (NonZeroU32::new(size.width), NonZeroU32::new(size.height)) + { + render_state.resize_surface(&mut surface, width, height); + } + + entry.insert(surface); + } + } else { + self.surfaces.clear(); + } + + Ok(()) + } + + pub fn paint( + &mut self, + viewport_id: ViewportId, + pixels_per_point: f32, + clipped_primitives: &[epaint::ClippedPrimitive], + textures_delta: &epaint::textures::TexturesDelta, + ) { + #[cfg(feature = "profiling")] + puffin::profile_function!(); + + let Some(render_state) = &mut self.render_state else { + return; + }; + let Some(surface) = self.surfaces.get(&viewport_id) else { + return; + }; + + let mut encoder = + render_state + .device + .create_command_encoder(&wgpu::CommandEncoderDescriptor { + label: Some("encoder"), + }); + + // Upload all resources for the GPU. + + let size_in_pixels = [surface.width, surface.height]; + let screen_descriptor = ScreenDescriptor { + size_in_pixels, + pixels_per_point, + }; + + for (id, image_delta) in &textures_delta.set { + render_state.update_texture(*id, image_delta); + } + render_state.update_buffers(clipped_primitives, &screen_descriptor); + + let output_frame = match surface.get_current_texture() { + Ok(frame) => frame, + Err(err) => { + if err != wgpu::SurfaceError::Outdated { + tracing::error!("failed to acquire next frame: {:?}", err); + } + return; + } + }; + + { + let view = match &surface.shader { + Some(shader) => &shader.view, + None => &output_frame + .texture + .create_view(&wgpu::TextureViewDescriptor::default()), + }; + let mut render_pass = encoder.begin_render_pass(&wgpu::RenderPassDescriptor { + label: Some("main_render_pass"), + color_attachments: &[Some(wgpu::RenderPassColorAttachment { + view, + resolve_target: None, + ops: wgpu::Operations { + load: wgpu::LoadOp::Clear(wgpu::Color::BLACK), + store: wgpu::StoreOp::Store, + }, + })], + depth_stencil_attachment: None, + timestamp_writes: None, + occlusion_query_set: None, + }); + + render_state.render(&mut render_pass, clipped_primitives, &screen_descriptor); + } + + if let Some(shader) = &surface.shader { + let view = &output_frame + .texture + .create_view(&wgpu::TextureViewDescriptor::default()); + + let mut render_pass = encoder.begin_render_pass(&wgpu::RenderPassDescriptor { + label: Some("main_render_pass"), + color_attachments: &[Some(wgpu::RenderPassColorAttachment { + view, + resolve_target: None, + ops: wgpu::Operations { + load: wgpu::LoadOp::Clear(wgpu::Color::BLACK), + store: wgpu::StoreOp::Store, + }, + })], + depth_stencil_attachment: None, + timestamp_writes: None, + occlusion_query_set: None, + }); + + render_pass.set_scissor_rect(0, 0, size_in_pixels[0], size_in_pixels[1]); + render_pass.set_viewport( + 0.0, + 0.0, + size_in_pixels[0] as f32, + size_in_pixels[1] as f32, + 0.0, + 1.0, + ); + render_pass.set_pipeline(&shader.render_pipeline); + render_pass.set_bind_group(0, &render_state.uniform_bind_group, &[]); + render_pass.set_bind_group(1, &shader.texture_bind_group, &[]); + render_pass.draw(0..3, 0..1); + } + + for id in &textures_delta.free { + render_state.textures.remove(id); + } + + render_state.queue.submit(iter::once(encoder.finish())); + + output_frame.present(); + } + + pub const fn render_state(&self) -> Option<&RenderState> { + self.render_state.as_ref() + } + + pub fn render_state_mut(&mut self) -> Option<&mut RenderState> { + self.render_state.as_mut() + } + + pub fn on_window_resized(&mut self, viewport_id: ViewportId, width: u32, height: u32) { + if let (Some(width), Some(height)) = (NonZeroU32::new(width), NonZeroU32::new(height)) { + if let Some(surface) = self.surfaces.get_mut(&viewport_id) { + if let Some(render_state) = &mut self.render_state { + render_state.resize_surface(surface, width, height); + } + } + } + } + + pub fn retain_surfaces(&mut self, viewport_ids: &ViewportIdSet) { + self.surfaces.retain(|id, _| viewport_ids.contains(id)); + } + + pub fn destroy(&mut self) { + self.surfaces.clear(); + let _ = self.render_state.take(); + } +} + +#[derive(Debug)] +#[must_use] +struct SlicedBuffer { + buffer: wgpu::Buffer, + slices: Vec>, + capacity: wgpu::BufferAddress, +} + +#[derive(Debug)] +#[must_use] +pub struct RenderState { + pub device: wgpu::Device, + pub queue: wgpu::Queue, + pub format: wgpu::TextureFormat, + + pipeline: wgpu::RenderPipeline, + + index_buffer: SlicedBuffer, + vertex_buffer: SlicedBuffer, + + uniform_buffer: wgpu::Buffer, + previous_uniform_buffer_content: UniformBuffer, + uniform_bind_group: wgpu::BindGroup, + uniform_bind_group_layout: wgpu::BindGroupLayout, + texture_bind_group_layout: wgpu::BindGroupLayout, + + shader: Shader, + /// Map of egui texture IDs to textures and their associated bindgroups (texture view + + /// sampler). The texture may be None if the `TextureId` is just a handle to a user-provided + /// sampler. + textures: HashMap, wgpu::BindGroup)>, + next_texture_id: u64, + samplers: HashMap, +} + +impl RenderState { + async fn create( + instance: &wgpu::Instance, + surface: &wgpu::Surface<'_>, + ) -> anyhow::Result { + let adapter = instance + .request_adapter(&wgpu::RequestAdapterOptions { + power_preference: wgpu::PowerPreference::HighPerformance, + compatible_surface: Some(surface), + force_fallback_adapter: false, + }) + .await + .context("failed to find suitable wgpu adapter")?; + + tracing::debug!("requested wgpu adapter: {:?}", adapter.get_info()); + + let base_limits = if adapter.get_info().backend == wgpu::Backend::Gl { + wgpu::Limits::downlevel_webgl2_defaults() + } else { + wgpu::Limits::default() + }; + let device_descriptor = wgpu::DeviceDescriptor { + label: Some("wgpu device"), + // TODO: maybe CLEAR_TEXTURE? + required_features: wgpu::Features::default(), + required_limits: wgpu::Limits { + max_texture_dimension_2d: 8192, + ..base_limits + }, + }; + let mut connection = adapter.request_device(&device_descriptor, None).await; + // Creating device may fail if adapter doesn't support the default cfg, so try to + // recover with lower limits. Specifically max_texture_dimension_2d has a downlevel default + // of 2048. egui_wgpu wants 8192 for 4k displays, but not all platforms support that yet. + if let Err(err) = connection { + tracing::error!("failed to create wgpu device: {err:?}, retrying with lower limits"); + connection = adapter + .request_device( + &wgpu::DeviceDescriptor { + required_limits: wgpu::Limits { + max_texture_dimension_2d: 4096, + ..base_limits + }, + ..device_descriptor + }, + None, + ) + .await + } + + let capabilities = surface.get_capabilities(&adapter); + let format = capabilities + .formats + .iter() + .copied() + .find(|format| { + // egui prefers these formats + matches!( + format, + wgpu::TextureFormat::Rgba8Unorm | wgpu::TextureFormat::Bgra8Unorm + ) + }) + .unwrap_or(capabilities.formats[0]); // TODO: Is falling back to first available okay? + + let (device, queue) = + connection.map_err(|err| anyhow!("failed to create wgpu device: {err:?}"))?; + + let shader_module_desc = + wgpu::include_wgsl!(concat!(env!("CARGO_MANIFEST_DIR"), "/shaders/gui.wgsl")); + let shader_module = device.create_shader_module(shader_module_desc); + + let uniform_buffer = device.create_buffer_init(&wgpu::util::BufferInitDescriptor { + label: Some("gui uniform buffer"), + contents: bytemuck::cast_slice(&[UniformBuffer::default()]), + usage: wgpu::BufferUsages::UNIFORM | wgpu::BufferUsages::COPY_DST, + }); + + let uniform_bind_group_layout = + device.create_bind_group_layout(&wgpu::BindGroupLayoutDescriptor { + label: Some("gui uniform bind group layout"), + entries: &[wgpu::BindGroupLayoutEntry { + binding: 0, + visibility: wgpu::ShaderStages::VERTEX | wgpu::ShaderStages::FRAGMENT, + ty: wgpu::BindingType::Buffer { + ty: wgpu::BufferBindingType::Uniform, + has_dynamic_offset: false, + min_binding_size: NonZeroU64::new( + std::mem::size_of::() as _, + ), + }, + count: None, + }], + }); + + let uniform_bind_group = device.create_bind_group(&wgpu::BindGroupDescriptor { + label: Some("gui uniform bind group"), + layout: &uniform_bind_group_layout, + entries: &[wgpu::BindGroupEntry { + binding: 0, + resource: wgpu::BindingResource::Buffer(wgpu::BufferBinding { + buffer: &uniform_buffer, + offset: 0, + size: None, + }), + }], + }); + + let texture_bind_group_layout = + device.create_bind_group_layout(&wgpu::BindGroupLayoutDescriptor { + label: Some("gui texture bind group layout"), + entries: &[ + wgpu::BindGroupLayoutEntry { + binding: 0, + visibility: wgpu::ShaderStages::FRAGMENT, + ty: wgpu::BindingType::Texture { + sample_type: wgpu::TextureSampleType::Float { filterable: true }, + multisampled: false, + view_dimension: wgpu::TextureViewDimension::D2, + }, + count: None, + }, + wgpu::BindGroupLayoutEntry { + binding: 1, + visibility: wgpu::ShaderStages::FRAGMENT, + ty: wgpu::BindingType::Sampler(wgpu::SamplerBindingType::Filtering), + count: None, + }, + ], + }); + + let pipeline_layout = device.create_pipeline_layout(&wgpu::PipelineLayoutDescriptor { + label: Some("gui pipeline layout"), + bind_group_layouts: &[&uniform_bind_group_layout, &texture_bind_group_layout], + push_constant_ranges: &[], + }); + + let pipeline = + device.create_render_pipeline(&wgpu::RenderPipelineDescriptor { + label: Some("gui pipeline"), + layout: Some(&pipeline_layout), + vertex: wgpu::VertexState { + entry_point: "vs_main", + module: &shader_module, + buffers: &[wgpu::VertexBufferLayout { + array_stride: 5 * 4, + step_mode: wgpu::VertexStepMode::Vertex, + // 0: vec2 position + // 1: vec2 uv coordinates + // 2: uint color + attributes: &wgpu::vertex_attr_array![0 => Float32x2, 1 => Float32x2, 2 => Uint32], + }], + compilation_options: wgpu::PipelineCompilationOptions::default() + }, + fragment: Some(wgpu::FragmentState { + module: &shader_module, + entry_point: "fs_main", + targets: &[Some(wgpu::ColorTargetState { + format, + blend: Some(wgpu::BlendState { + color: wgpu::BlendComponent { + src_factor: wgpu::BlendFactor::One, + dst_factor: wgpu::BlendFactor::OneMinusSrcAlpha, + operation: wgpu::BlendOperation::Add, + }, + alpha: wgpu::BlendComponent { + src_factor: wgpu::BlendFactor::OneMinusDstAlpha, + dst_factor: wgpu::BlendFactor::One, + operation: wgpu::BlendOperation::Add, + }, + }), + write_mask: wgpu::ColorWrites::ALL, + })], + compilation_options: wgpu::PipelineCompilationOptions::default() + }), + primitive: wgpu::PrimitiveState::default(), + depth_stencil: None, + multisample: wgpu::MultisampleState::default(), + multiview: None, + } + ); + + const INDEX_BUFFER_START_CAPACITY: wgpu::BufferAddress = + (std::mem::size_of::() * 1024 * 3) as _; + const VERTEX_BUFFER_START_CAPACITY: wgpu::BufferAddress = + (std::mem::size_of::() * 1024) as _; + + let index_buffer = SlicedBuffer { + buffer: Self::create_index_buffer(&device, INDEX_BUFFER_START_CAPACITY), + slices: Vec::with_capacity(64), + capacity: INDEX_BUFFER_START_CAPACITY, + }; + let vertex_buffer = SlicedBuffer { + buffer: Self::create_vertex_buffer(&device, VERTEX_BUFFER_START_CAPACITY), + slices: Vec::with_capacity(64), + capacity: VERTEX_BUFFER_START_CAPACITY, + }; + + Ok(Self { + device, + queue, + format, + + pipeline, + + index_buffer, + vertex_buffer, + + uniform_buffer, + previous_uniform_buffer_content: Default::default(), + uniform_bind_group, + uniform_bind_group_layout, + texture_bind_group_layout, + + shader: Shader::default(), + textures: Default::default(), + next_texture_id: 0, + samplers: Default::default(), + }) + } + + pub fn set_shader(&mut self, shader: Shader) { + self.shader = shader; + } + + pub fn max_texture_side(&self) -> u32 { + self.device.limits().max_texture_dimension_2d + } + + pub fn register_texture( + &mut self, + label: Option<&str>, + view: &wgpu::TextureView, + sampler_descriptor: wgpu::SamplerDescriptor<'_>, + ) -> epaint::TextureId { + let sampler = self.device.create_sampler(&sampler_descriptor); + let bind_group = self.device.create_bind_group(&wgpu::BindGroupDescriptor { + label, + layout: &self.texture_bind_group_layout, + entries: &[ + wgpu::BindGroupEntry { + binding: 0, + resource: wgpu::BindingResource::TextureView(view), + }, + wgpu::BindGroupEntry { + binding: 1, + resource: wgpu::BindingResource::Sampler(&sampler), + }, + ], + }); + + let id = epaint::TextureId::User(self.next_texture_id); + self.textures.insert(id, (None, bind_group)); + self.next_texture_id += 1; + + id + } + + fn create_vertex_buffer(device: &wgpu::Device, size: u64) -> wgpu::Buffer { + device.create_buffer(&wgpu::BufferDescriptor { + label: Some("gui vertex buffer"), + usage: wgpu::BufferUsages::VERTEX | wgpu::BufferUsages::COPY_DST, + size, + mapped_at_creation: false, + }) + } + + fn create_index_buffer(device: &wgpu::Device, size: u64) -> wgpu::Buffer { + device.create_buffer(&wgpu::BufferDescriptor { + label: Some("gui index buffer"), + usage: wgpu::BufferUsages::INDEX | wgpu::BufferUsages::COPY_DST, + size, + mapped_at_creation: false, + }) + } + + fn resize_surface(&self, surface: &mut Surface, width: NonZeroU32, height: NonZeroU32) { + surface.width = width.get(); + surface.height = height.get(); + surface.configure( + &self.device, + &wgpu::SurfaceConfiguration { + usage: wgpu::TextureUsages::RENDER_ATTACHMENT, + format: self.format, + width: width.get(), + height: height.get(), + // TODO: Support disabling vsync + present_mode: wgpu::PresentMode::AutoVsync, + desired_maximum_frame_latency: 2, + alpha_mode: wgpu::CompositeAlphaMode::Auto, + view_formats: vec![self.format], + }, + ); + surface.set_shader( + &self.device, + self.format, + &self.uniform_bind_group_layout, + self.shader, + ); + } + + pub fn update_texture(&mut self, id: epaint::TextureId, image_delta: &epaint::ImageDelta) { + let width = image_delta.image.width() as u32; + let height = image_delta.image.height() as u32; + + let size = wgpu::Extent3d { + width, + height, + depth_or_array_layers: 1, + }; + + let data_color32 = match &image_delta.image { + epaint::ImageData::Color(image) => { + assert_eq!( + width as usize * height as usize, + image.pixels.len(), + "Mismatch between texture size and texel count" + ); + Cow::Borrowed(&image.pixels) + } + epaint::ImageData::Font(image) => { + assert_eq!( + width as usize * height as usize, + image.pixels.len(), + "Mismatch between texture size and texel count" + ); + Cow::Owned(image.srgba_pixels(None).collect::>()) + } + }; + let data_bytes: &[u8] = bytemuck::cast_slice(data_color32.as_slice()); + + let queue_write_data_to_texture = |texture, origin| { + self.queue.write_texture( + wgpu::ImageCopyTexture { + texture, + mip_level: 0, + origin, + aspect: wgpu::TextureAspect::All, + }, + data_bytes, + wgpu::ImageDataLayout { + offset: 0, + bytes_per_row: Some(4 * width), + rows_per_image: Some(height), + }, + size, + ); + }; + + if let Some(pos) = image_delta.pos { + // update the existing texture + let (texture, _bind_group) = self + .textures + .get(&id) + .expect("Tried to update a texture that has not been allocated yet."); + let origin = wgpu::Origin3d { + x: pos[0] as u32, + y: pos[1] as u32, + z: 0, + }; + queue_write_data_to_texture( + texture.as_ref().expect("Tried to update user texture."), + origin, + ); + } else { + // allocate a new texture + // Use same label for all resources associated with this texture id (no point in retyping the type) + let label_str = format!("texture_{id:?}"); + let label = Some(label_str.as_str()); + let texture = { + self.device.create_texture(&wgpu::TextureDescriptor { + label, + size, + mip_level_count: 1, + sample_count: 1, + dimension: wgpu::TextureDimension::D2, + format: wgpu::TextureFormat::Rgba8UnormSrgb, // Minspec for wgpu WebGL emulation is WebGL2, so this should always be supported. + usage: wgpu::TextureUsages::TEXTURE_BINDING | wgpu::TextureUsages::COPY_DST, + view_formats: &[wgpu::TextureFormat::Rgba8UnormSrgb], + }) + }; + let sampler = self + .samplers + .entry(image_delta.options) + .or_insert_with(|| Self::create_sampler(image_delta.options, &self.device)); + + let view = texture.create_view(&wgpu::TextureViewDescriptor::default()); + let bind_group = self.device.create_bind_group(&wgpu::BindGroupDescriptor { + label, + layout: &self.texture_bind_group_layout, + entries: &[ + wgpu::BindGroupEntry { + binding: 0, + resource: wgpu::BindingResource::TextureView(&view), + }, + wgpu::BindGroupEntry { + binding: 1, + resource: wgpu::BindingResource::Sampler(sampler), + }, + ], + }); + let origin = wgpu::Origin3d::ZERO; + queue_write_data_to_texture(&texture, origin); + self.textures.insert(id, (Some(texture), bind_group)); + }; + } + + pub fn update_buffers( + &mut self, + paint_jobs: &[epaint::ClippedPrimitive], + screen_descriptor: &ScreenDescriptor, + ) { + let screen_size_in_points = screen_descriptor.screen_size_in_points(); + + let uniform_buffer_content = UniformBuffer { + screen_size_in_points, + _padding: Default::default(), + }; + if uniform_buffer_content != self.previous_uniform_buffer_content { + self.queue.write_buffer( + &self.uniform_buffer, + 0, + bytemuck::cast_slice(&[uniform_buffer_content]), + ); + self.previous_uniform_buffer_content = uniform_buffer_content; + } + + // Determine how many vertices & indices need to be rendered, and gather prepare callbacks + // let mut callbacks = Vec::new(); + let (vertex_count, index_count) = + paint_jobs.iter().fold((0, 0), |acc, clipped_primitive| { + if let Primitive::Mesh(mesh) = &clipped_primitive.primitive { + (acc.0 + mesh.vertices.len(), acc.1 + mesh.indices.len()) + } else { + acc + } + }); + + if index_count > 0 { + self.index_buffer.slices.clear(); + + let required_index_buffer_size = (std::mem::size_of::() * index_count) as u64; + if self.index_buffer.capacity < required_index_buffer_size { + // Resize index buffer if needed. + self.index_buffer.capacity = + (self.index_buffer.capacity * 2).at_least(required_index_buffer_size); + self.index_buffer.buffer = + Self::create_index_buffer(&self.device, self.index_buffer.capacity); + } + + let index_buffer_staging = self.queue.write_buffer_with( + &self.index_buffer.buffer, + 0, + NonZeroU64::new(required_index_buffer_size).expect("valid index buffer size"), + ); + + let Some(mut index_buffer_staging) = index_buffer_staging else { + panic!("Failed to create staging buffer for index data. Index count: {index_count}. Required index buffer size: {required_index_buffer_size}. Actual size {} and capacity: {} (bytes)", self.index_buffer.buffer.size(), self.index_buffer.capacity); + }; + + let mut index_offset = 0; + for epaint::ClippedPrimitive { primitive, .. } in paint_jobs { + if let Primitive::Mesh(mesh) = primitive { + let size = mesh.indices.len() * std::mem::size_of::(); + let slice = index_offset..(size + index_offset); + index_buffer_staging[slice.clone()] + .copy_from_slice(bytemuck::cast_slice(&mesh.indices)); + self.index_buffer.slices.push(slice); + index_offset += size; + } + } + } + + if vertex_count > 0 { + self.vertex_buffer.slices.clear(); + + let required_vertex_buffer_size = (std::mem::size_of::() * vertex_count) as u64; + if self.vertex_buffer.capacity < required_vertex_buffer_size { + // Resize vertex buffer if needed. + self.vertex_buffer.capacity = + (self.vertex_buffer.capacity * 2).at_least(required_vertex_buffer_size); + self.vertex_buffer.buffer = + Self::create_vertex_buffer(&self.device, self.vertex_buffer.capacity); + } + + let vertex_buffer_staging = self.queue.write_buffer_with( + &self.vertex_buffer.buffer, + 0, + NonZeroU64::new(required_vertex_buffer_size).expect("valid vertex buffer size"), + ); + + let Some(mut vertex_buffer_staging) = vertex_buffer_staging else { + panic!("Failed to create staging buffer for vertex data. Vertex count: {vertex_count}. Required vertex buffer size: {required_vertex_buffer_size}. Actual size {} and capacity: {} (bytes)", self.vertex_buffer.buffer.size(), self.vertex_buffer.capacity); + }; + + let mut vertex_offset = 0; + for epaint::ClippedPrimitive { primitive, .. } in paint_jobs { + if let Primitive::Mesh(mesh) = primitive { + let size = mesh.vertices.len() * std::mem::size_of::(); + let slice = vertex_offset..(size + vertex_offset); + vertex_buffer_staging[slice.clone()] + .copy_from_slice(bytemuck::cast_slice(&mesh.vertices)); + self.vertex_buffer.slices.push(slice); + vertex_offset += size; + } + } + } + } + + pub fn render<'rp>( + &'rp self, + render_pass: &mut wgpu::RenderPass<'rp>, + paint_jobs: &'rp [epaint::ClippedPrimitive], + screen_descriptor: &ScreenDescriptor, + ) { + let pixels_per_point = screen_descriptor.pixels_per_point; + let size_in_pixels = screen_descriptor.size_in_pixels; + + render_pass.set_scissor_rect(0, 0, size_in_pixels[0], size_in_pixels[1]); + render_pass.set_viewport( + 0.0, + 0.0, + size_in_pixels[0] as f32, + size_in_pixels[1] as f32, + 0.0, + 1.0, + ); + render_pass.set_pipeline(&self.pipeline); + render_pass.set_bind_group(0, &self.uniform_bind_group, &[]); + + let mut index_buffer_slices = self.index_buffer.slices.iter(); + let mut vertex_buffer_slices = self.vertex_buffer.slices.iter(); + + for epaint::ClippedPrimitive { + clip_rect, + primitive, + } in paint_jobs + { + let rect = ScissorRect::new(clip_rect, pixels_per_point, size_in_pixels); + + if rect.width == 0 || rect.height == 0 { + // Skip rendering zero-sized clip areas. + if let Primitive::Mesh(_) = primitive { + // If this is a mesh, we need to advance the index and vertex buffer iterators: + index_buffer_slices.next(); + vertex_buffer_slices.next(); + } + continue; + } + + render_pass.set_scissor_rect(rect.x, rect.y, rect.width, rect.height); + + if let Primitive::Mesh(mesh) = primitive { + // These expects should be valid because update_buffers inserts a slice for every + // primitive + let index_buffer_slice = index_buffer_slices + .next() + .expect("valid index buffer slice"); + let vertex_buffer_slice = vertex_buffer_slices + .next() + .expect("valid vertex buffer slice"); + + if let Some((_texture, bind_group)) = self.textures.get(&mesh.texture_id) { + render_pass.set_bind_group(1, bind_group, &[]); + render_pass.set_index_buffer( + self.index_buffer + .buffer + .slice(index_buffer_slice.start as u64..index_buffer_slice.end as u64), + wgpu::IndexFormat::Uint32, + ); + render_pass.set_vertex_buffer( + 0, + self.vertex_buffer.buffer.slice( + vertex_buffer_slice.start as u64..vertex_buffer_slice.end as u64, + ), + ); + render_pass.draw_indexed(0..mesh.indices.len() as u32, 0, 0..1); + } else { + tracing::warn!("Missing texture: {:?}", mesh.texture_id); + } + } + } + + render_pass.set_scissor_rect(0, 0, size_in_pixels[0], size_in_pixels[1]); + } + + fn create_sampler( + options: epaint::textures::TextureOptions, + device: &wgpu::Device, + ) -> wgpu::Sampler { + let mag_filter = match options.magnification { + epaint::textures::TextureFilter::Nearest => wgpu::FilterMode::Nearest, + epaint::textures::TextureFilter::Linear => wgpu::FilterMode::Linear, + }; + let min_filter = match options.minification { + epaint::textures::TextureFilter::Nearest => wgpu::FilterMode::Nearest, + epaint::textures::TextureFilter::Linear => wgpu::FilterMode::Linear, + }; + let address_mode = match options.wrap_mode { + epaint::textures::TextureWrapMode::ClampToEdge => wgpu::AddressMode::ClampToEdge, + epaint::textures::TextureWrapMode::Repeat => wgpu::AddressMode::Repeat, + epaint::textures::TextureWrapMode::MirroredRepeat => wgpu::AddressMode::MirrorRepeat, + }; + device.create_sampler(&wgpu::SamplerDescriptor { + label: Some(&format!( + "gui sampler (mag: {mag_filter:?}, min {min_filter:?})" + )), + mag_filter, + min_filter, + address_mode_u: address_mode, + address_mode_v: address_mode, + ..Default::default() + }) + } +} + +/// Uniform buffer used when rendering. +#[derive(Default, Debug, Copy, Clone, bytemuck::Pod, bytemuck::Zeroable)] +#[repr(C)] +struct UniformBuffer { + screen_size_in_points: [f32; 2], + // Uniform buffers need to be at least 16 bytes in WebGL. + // See https://github.com/gfx-rs/wgpu/issues/2072 + _padding: [u32; 2], +} + +impl PartialEq for UniformBuffer { + fn eq(&self, other: &Self) -> bool { + self.screen_size_in_points == other.screen_size_in_points + } +} + +/// Information about the screen used for rendering. +pub struct ScreenDescriptor { + /// Size of the window in physical pixels. + pub size_in_pixels: [u32; 2], + + /// HiDPI scale factor (pixels per point). + pub pixels_per_point: f32, +} + +impl ScreenDescriptor { + /// size in "logical" points + fn screen_size_in_points(&self) -> [f32; 2] { + [ + self.size_in_pixels[0] as f32 / self.pixels_per_point, + self.size_in_pixels[1] as f32 / self.pixels_per_point, + ] + } +} + +/// A Rect in physical pixel space, used for setting clipping rectangles. +struct ScissorRect { + x: u32, + y: u32, + width: u32, + height: u32, +} + +impl ScissorRect { + fn new(clip_rect: &epaint::Rect, pixels_per_point: f32, target_size: [u32; 2]) -> Self { + // Transform clip rect to physical pixels: + let clip_min_x = pixels_per_point * clip_rect.min.x; + let clip_min_y = pixels_per_point * clip_rect.min.y; + let clip_max_x = pixels_per_point * clip_rect.max.x; + let clip_max_y = pixels_per_point * clip_rect.max.y; + + // Round to integer: + let clip_min_x = clip_min_x.round() as u32; + let clip_min_y = clip_min_y.round() as u32; + let clip_max_x = clip_max_x.round() as u32; + let clip_max_y = clip_max_y.round() as u32; + + // Clamp: + let clip_min_x = clip_min_x.clamp(0, target_size[0]); + let clip_min_y = clip_min_y.clamp(0, target_size[1]); + let clip_max_x = clip_max_x.clamp(clip_min_x, target_size[0]); + let clip_max_y = clip_max_y.clamp(clip_min_y, target_size[1]); + + Self { + x: clip_min_x, + y: clip_min_y, + width: clip_max_x - clip_min_x, + height: clip_max_y - clip_min_y, + } + } +} diff --git a/tetanes/src/nes/renderer/shader.rs b/tetanes/src/nes/renderer/shader.rs index 563cf5e7..a07452b6 100644 --- a/tetanes/src/nes/renderer/shader.rs +++ b/tetanes/src/nes/renderer/shader.rs @@ -1,7 +1,4 @@ -use egui::Rect; -use egui_wgpu::RenderState; use serde::{Deserialize, Serialize}; -use std::num::NonZeroU64; use thiserror::Error; #[derive(Error, Debug)] @@ -44,144 +41,22 @@ impl TryFrom for Shader { } } -#[derive(Debug)] -#[must_use] -pub struct Renderer { - rect: Rect, -} - -impl Renderer { - pub const fn new(rect: Rect) -> Self { - Self { rect } - } -} - -impl egui_wgpu::CallbackTrait for Renderer { - fn prepare( - &self, - _device: &wgpu::Device, - queue: &wgpu::Queue, - _screen_descriptor: &egui_wgpu::ScreenDescriptor, - _egui_encoder: &mut wgpu::CommandEncoder, - resources: &mut egui_wgpu::CallbackResources, - ) -> Vec { - if let Some(shader_res) = resources.get::() { - queue.write_buffer( - &shader_res.size_uniform, - 0, - bytemuck::cast_slice(&[self.rect.width(), self.rect.height(), 0.0, 0.0]), - ); - } - Vec::new() - } - - fn paint<'a>( - &'a self, - _info: egui::PaintCallbackInfo, - render_pass: &mut wgpu::RenderPass<'a>, - resources: &'a egui_wgpu::CallbackResources, - ) { - if let Some(shader_res) = resources.get::() { - render_pass.set_pipeline(&shader_res.render_pipeline); - render_pass.set_bind_group(0, &shader_res.bind_group, &[]); - render_pass.draw(0..3, 0..1); - } - } -} - #[derive(Debug)] #[must_use] pub struct Resources { - bind_group: wgpu::BindGroup, - size_uniform: wgpu::Buffer, - render_pipeline: wgpu::RenderPipeline, + pub view: wgpu::TextureView, + pub texture_bind_group: wgpu::BindGroup, + pub render_pipeline: wgpu::RenderPipeline, } impl Resources { - pub fn new(render_state: &RenderState, view: &wgpu::TextureView, shader: Shader) -> Self { - let size_uniform_size = 16; - let size_uniform = render_state.device.create_buffer(&wgpu::BufferDescriptor { - label: Some("Frame Size Buffer"), - size: size_uniform_size, // 16-byte minimum alignment, even though we only need 8 bytes - usage: wgpu::BufferUsages::COPY_DST | wgpu::BufferUsages::UNIFORM, - mapped_at_creation: false, - }); - - let bind_group_layout = - render_state - .device - .create_bind_group_layout(&wgpu::BindGroupLayoutDescriptor { - label: Some("bind group layout"), - entries: &[ - wgpu::BindGroupLayoutEntry { - binding: 0, - visibility: wgpu::ShaderStages::FRAGMENT, - ty: wgpu::BindingType::Buffer { - ty: wgpu::BufferBindingType::Uniform, - has_dynamic_offset: false, - min_binding_size: NonZeroU64::new(size_uniform_size), - }, - count: None, - }, - wgpu::BindGroupLayoutEntry { - binding: 1, - visibility: wgpu::ShaderStages::VERTEX | wgpu::ShaderStages::FRAGMENT, - ty: wgpu::BindingType::Texture { - sample_type: wgpu::TextureSampleType::Float { filterable: true }, - multisampled: false, - view_dimension: wgpu::TextureViewDimension::D2, - }, - count: None, - }, - wgpu::BindGroupLayoutEntry { - binding: 2, - visibility: wgpu::ShaderStages::FRAGMENT, - ty: wgpu::BindingType::Sampler(wgpu::SamplerBindingType::Filtering), - count: None, - }, - ], - }); - let sampler = render_state - .device - .create_sampler(&wgpu::SamplerDescriptor { - label: Some("sampler"), - address_mode_u: wgpu::AddressMode::ClampToEdge, - address_mode_v: wgpu::AddressMode::ClampToEdge, - address_mode_w: wgpu::AddressMode::ClampToEdge, - mag_filter: wgpu::FilterMode::Nearest, - min_filter: wgpu::FilterMode::Nearest, - mipmap_filter: wgpu::FilterMode::Nearest, - ..Default::default() - }); - let bind_group = render_state - .device - .create_bind_group(&wgpu::BindGroupDescriptor { - label: Some("nes frame bind group"), - layout: &bind_group_layout, - entries: &[ - wgpu::BindGroupEntry { - binding: 0, - resource: size_uniform.as_entire_binding(), - }, - wgpu::BindGroupEntry { - binding: 1, - resource: wgpu::BindingResource::TextureView(view), - }, - wgpu::BindGroupEntry { - binding: 2, - resource: wgpu::BindingResource::Sampler(&sampler), - }, - ], - }); - let pipeline_layout = - render_state - .device - .create_pipeline_layout(&wgpu::PipelineLayoutDescriptor { - label: Some("pipeline layout"), - bind_group_layouts: &[&bind_group_layout], - push_constant_ranges: &[], - }); - + pub fn new( + device: &wgpu::Device, + format: wgpu::TextureFormat, + view: wgpu::TextureView, + uniform_bind_group_layout: &wgpu::BindGroupLayout, + shader: Shader, + ) -> Self { let shader_module_desc = match shader { Shader::None => panic!("No shader selected"), Shader::CrtEasymode => wgpu::include_wgsl!(concat!( @@ -189,40 +64,88 @@ impl Resources { "/shaders/crt-easymode.wgsl" )), }; - let shader = render_state.device.create_shader_module(shader_module_desc); + let shader_module = device.create_shader_module(shader_module_desc); - let render_pipeline = - render_state - .device - .create_render_pipeline(&wgpu::RenderPipelineDescriptor { - label: Some("render pipeline"), - layout: Some(&pipeline_layout), - vertex: wgpu::VertexState { - module: &shader, - entry_point: "vs_main", - buffers: &[], + let texture_bind_group_layout = + device.create_bind_group_layout(&wgpu::BindGroupLayoutDescriptor { + label: Some("bind group layout"), + entries: &[ + wgpu::BindGroupLayoutEntry { + binding: 0, + visibility: wgpu::ShaderStages::VERTEX | wgpu::ShaderStages::FRAGMENT, + ty: wgpu::BindingType::Texture { + sample_type: wgpu::TextureSampleType::Float { filterable: true }, + multisampled: false, + view_dimension: wgpu::TextureViewDimension::D2, + }, + count: None, }, - fragment: Some(wgpu::FragmentState { - module: &shader, - entry_point: "fs_main", - targets: &[Some(wgpu::ColorTargetState { - format: render_state.target_format, - blend: Some(wgpu::BlendState::REPLACE), - write_mask: wgpu::ColorWrites::ALL, - })], - }), - primitive: wgpu::PrimitiveState { - cull_mode: Some(wgpu::Face::Back), - ..Default::default() + wgpu::BindGroupLayoutEntry { + binding: 1, + visibility: wgpu::ShaderStages::FRAGMENT, + ty: wgpu::BindingType::Sampler(wgpu::SamplerBindingType::Filtering), + count: None, }, - depth_stencil: None, - multisample: wgpu::MultisampleState::default(), - multiview: None, - }); + ], + }); + let sampler = device.create_sampler(&wgpu::SamplerDescriptor { + label: Some("sampler"), + address_mode_u: wgpu::AddressMode::ClampToEdge, + address_mode_v: wgpu::AddressMode::ClampToEdge, + address_mode_w: wgpu::AddressMode::ClampToEdge, + mag_filter: wgpu::FilterMode::Nearest, + min_filter: wgpu::FilterMode::Nearest, + mipmap_filter: wgpu::FilterMode::Nearest, + ..Default::default() + }); + let texture_bind_group = device.create_bind_group(&wgpu::BindGroupDescriptor { + label: Some("nes frame bind group"), + layout: &texture_bind_group_layout, + entries: &[ + wgpu::BindGroupEntry { + binding: 0, + resource: wgpu::BindingResource::TextureView(&view), + }, + wgpu::BindGroupEntry { + binding: 1, + resource: wgpu::BindingResource::Sampler(&sampler), + }, + ], + }); + let pipeline_layout = device.create_pipeline_layout(&wgpu::PipelineLayoutDescriptor { + label: Some("shader pipeline layout"), + bind_group_layouts: &[uniform_bind_group_layout, &texture_bind_group_layout], + push_constant_ranges: &[], + }); + + let render_pipeline = device.create_render_pipeline(&wgpu::RenderPipelineDescriptor { + label: Some("render pipeline"), + layout: Some(&pipeline_layout), + vertex: wgpu::VertexState { + module: &shader_module, + entry_point: "vs_main", + buffers: &[], + compilation_options: wgpu::PipelineCompilationOptions::default(), + }, + fragment: Some(wgpu::FragmentState { + module: &shader_module, + entry_point: "fs_main", + targets: &[Some(wgpu::ColorTargetState { + format, + blend: None, + write_mask: wgpu::ColorWrites::ALL, + })], + compilation_options: wgpu::PipelineCompilationOptions::default(), + }), + primitive: wgpu::PrimitiveState::default(), + depth_stencil: None, + multisample: wgpu::MultisampleState::default(), + multiview: None, + }); Self { - bind_group, - size_uniform, + view, + texture_bind_group, render_pipeline, } } diff --git a/tetanes/src/nes/renderer/texture.rs b/tetanes/src/nes/renderer/texture.rs index 69af4f9b..5885d2c6 100644 --- a/tetanes/src/nes/renderer/texture.rs +++ b/tetanes/src/nes/renderer/texture.rs @@ -1,3 +1,4 @@ +use crate::nes::renderer::painter::RenderState; use egui::{load::SizedTexture, TextureId, Vec2}; #[derive(Debug)] @@ -13,28 +14,29 @@ pub struct Texture { impl Texture { pub fn new( - device: &wgpu::Device, - renderer: &mut egui_wgpu::Renderer, - width: u32, - height: u32, + render_state: &mut RenderState, + size: Vec2, aspect_ratio: f32, label: Option<&'static str>, ) -> Self { + let max_texture_side = render_state.max_texture_side() as f32; let size = wgpu::Extent3d { - width, - height, + width: size.x.min(max_texture_side) as u32, + height: size.y.min(max_texture_side) as u32, depth_or_array_layers: 1, }; - let texture = device.create_texture(&wgpu::TextureDescriptor { - label, - size, - mip_level_count: 1, - sample_count: 1, - dimension: wgpu::TextureDimension::D2, - format: wgpu::TextureFormat::Rgba8UnormSrgb, - usage: wgpu::TextureUsages::TEXTURE_BINDING | wgpu::TextureUsages::COPY_DST, - view_formats: &[], - }); + let texture = render_state + .device + .create_texture(&wgpu::TextureDescriptor { + label, + size, + mip_level_count: 1, + sample_count: 1, + dimension: wgpu::TextureDimension::D2, + format: wgpu::TextureFormat::Rgba8UnormSrgb, + usage: wgpu::TextureUsages::TEXTURE_BINDING | wgpu::TextureUsages::COPY_DST, + view_formats: &[], + }); let view = texture.create_view(&wgpu::TextureViewDescriptor { label, @@ -51,12 +53,7 @@ impl Texture { mipmap_filter: wgpu::FilterMode::Nearest, ..Default::default() }; - - let id = renderer.register_native_texture_with_sampler_options( - device, - &view, - sampler_descriptor, - ); + let id = render_state.register_texture(label, &view, sampler_descriptor); Self { label, @@ -68,16 +65,8 @@ impl Texture { } } - pub fn resize( - &mut self, - device: &wgpu::Device, - renderer: &mut egui_wgpu::Renderer, - width: u32, - height: u32, - aspect_ratio: f32, - ) { - renderer.free_texture(&self.id); - *self = Self::new(device, renderer, width, height, aspect_ratio, self.label); + pub fn resize(&mut self, render_state: &mut RenderState, size: Vec2, aspect_ratio: f32) { + *self = Self::new(render_state, size, aspect_ratio, self.label); } pub fn sized_texture(&self) -> SizedTexture { diff --git a/tetanes/src/platform.rs b/tetanes/src/platform.rs index 59b96c22..1636f1a8 100644 --- a/tetanes/src/platform.rs +++ b/tetanes/src/platform.rs @@ -1,6 +1,5 @@ use crate::sys::platform; use std::path::{Path, PathBuf}; -use winit::{event::Event, event_loop::EventLoopWindowTarget}; pub use platform::*; @@ -16,14 +15,6 @@ pub trait BuilderExt { fn with_platform(self, title: &str) -> Self; } -/// Extension trait for `EventLoop` that provides platform-specific behavior. -pub trait EventLoopExt { - /// Runs the event loop for the current platform. - fn run_platform(self, event_handler: F) -> anyhow::Result<()> - where - F: FnMut(Event, &EventLoopWindowTarget) + 'static; -} - /// Method for platforms supporting opening a file dialog. pub fn open_file_dialog( title: impl Into, @@ -42,14 +33,13 @@ pub fn speak_text(text: &str) { pub mod renderer { use super::*; - use crate::nes::{config::Config, renderer::Renderer}; - use egui_winit::EventResponse; + use crate::nes::{config::Config, event::Response, renderer::Renderer}; pub fn constrain_window_to_viewport( renderer: &Renderer, desired_window_width: f32, cfg: &Config, - ) -> EventResponse { + ) -> Response { platform::renderer::constrain_window_to_viewport_impl(renderer, desired_window_width, cfg) } } @@ -59,16 +49,14 @@ pub mod renderer { #[must_use] pub enum Feature { AbortOnExit, - AccessKit, Blocking, ConstrainedViewport, ConsumePaste, - DeferredViewport, Filesystem, ScreenReader, Storage, Suspend, - Viewports, + OsViewports, } /// Checks if the current platform supports a given feature. @@ -79,9 +67,7 @@ macro_rules! feature { match $feature { // Wasm should never be able to exit AbortOnExit => cfg!(target_arch = "wasm32"), - // FIXME: Deadlock thread sleep issue with zbus/async-io on linux when menus are opened - AccessKit => cfg!(any(target_os = "macos", target_os = "windows")), - Blocking | DeferredViewport | Filesystem | Viewports => { + Blocking | Filesystem | OsViewports => { cfg!(not(target_arch = "wasm32")) } ConstrainedViewport | ConsumePaste | ScreenReader => { diff --git a/tetanes/src/sys/platform/os.rs b/tetanes/src/sys/platform/os.rs index b8792a75..6b6c033b 100644 --- a/tetanes/src/sys/platform/os.rs +++ b/tetanes/src/sys/platform/os.rs @@ -1,14 +1,10 @@ use crate::{ nes::{event::EmulationEvent, renderer::Renderer, Running}, - platform::{BuilderExt, EventLoopExt, Initialize}, + platform::{BuilderExt, Initialize}, }; use std::path::{Path, PathBuf}; use tracing::error; -use winit::{ - event::Event, - event_loop::{EventLoop, EventLoopWindowTarget}, - window::WindowBuilder, -}; +use winit::window::WindowAttributes; /// Method for platforms supporting opening a file dialog. pub fn open_file_dialog_impl( @@ -53,7 +49,7 @@ impl Initialize for Renderer { } } -impl BuilderExt for WindowBuilder { +impl BuilderExt for WindowAttributes { /// Sets platform-specific window options. fn with_platform(self, _title: &str) -> Self { use anyhow::Context; @@ -66,7 +62,7 @@ impl BuilderExt for WindowBuilder { .decode() .context("failed to decode window icon"); - let window_builder = self.with_window_icon( + let window_attrs = self.with_window_icon( icon.and_then(|png| { let width = png.width(); let height = png.height(); @@ -77,41 +73,37 @@ impl BuilderExt for WindowBuilder { .ok(), ); + #[cfg(target_os = "linux")] + let window_attrs = { + use winit::platform::wayland::WindowAttributesExtWayland as _; + + window_attrs.with_name(_title, "") + }; + // Ensures that viewport windows open in a separate window instead of a tab, which has // issues with certain preference toggles like fullscreen that effect the root viewport. #[cfg(target_os = "macos")] - let window_builder = { - use winit::platform::macos::{OptionAsAlt, WindowBuilderExtMacOS}; + let window_attrs = { + use winit::platform::macos::{OptionAsAlt, WindowAttributesExtMacOS}; - window_builder + window_attrs .with_tabbing_identifier(_title) .with_option_as_alt(OptionAsAlt::Both) }; - window_builder - } -} -impl EventLoopExt for EventLoop { - /// Runs the event loop for the current platform. - fn run_platform(self, event_handler: F) -> anyhow::Result<()> - where - F: FnMut(Event, &EventLoopWindowTarget) + 'static, - { - self.run(event_handler)?; - Ok(()) + window_attrs } } pub mod renderer { use super::*; - use crate::nes::config::Config; - use egui_winit::EventResponse; + use crate::nes::{config::Config, event::Response}; pub fn constrain_window_to_viewport_impl( _renderer: &Renderer, _desired_window_width: f32, _cfg: &Config, - ) -> EventResponse { - EventResponse::default() + ) -> Response { + Response::default() } } diff --git a/tetanes/src/sys/platform/wasm.rs b/tetanes/src/sys/platform/wasm.rs index 3a2121b8..43a8f5ef 100644 --- a/tetanes/src/sys/platform/wasm.rs +++ b/tetanes/src/sys/platform/wasm.rs @@ -1,11 +1,11 @@ use crate::{ nes::{ event::{EmulationEvent, NesEventProxy, RendererEvent, ReplayData, UiEvent}, - renderer::Renderer, + renderer::{gui, Renderer, State}, rom::RomData, Running, }, - platform::{BuilderExt, EventLoopExt, Initialize}, + platform::{BuilderExt, Initialize}, thread, }; use anyhow::{bail, Context}; @@ -17,12 +17,7 @@ use wasm_bindgen::prelude::*; use web_sys::{ js_sys::Uint8Array, FileReader, HtmlAnchorElement, HtmlCanvasElement, HtmlInputElement, }; -use winit::{ - event::Event, - event_loop::{EventLoop, EventLoopWindowTarget}, - platform::web::{EventLoopExtWebSys, WindowBuilderExtWebSys}, - window::WindowBuilder, -}; +use winit::{platform::web::WindowAttributesExtWebSys, window::WindowAttributes}; const BIN_NAME: &str = env!("CARGO_PKG_NAME"); const VERSION: &str = env!("CARGO_PKG_VERSION"); @@ -223,9 +218,9 @@ pub mod renderer { use super::*; use crate::nes::{ config::Config, - renderer::{gui::Gui, State}, + event::Response, + renderer::{gui::Gui, Viewport}, }; - use egui_winit::EventResponse; use std::cell::RefCell; use wasm_bindgen_futures::JsFuture; use winit::dpi::LogicalSize; @@ -234,7 +229,7 @@ pub mod renderer { renderer: &Renderer, desired_window_width: f32, cfg: &Config, - ) -> EventResponse { + ) -> Response { if let Some(window) = renderer.root_window() { if let Some(canvas) = crate::platform::get_canvas() { // Can't use `Window::inner_size` here because it's reported incorrectly so @@ -266,7 +261,7 @@ pub mod renderer { new_window_size.y, )); } - return EventResponse { + return Response { consumed: true, repaint: true, }; @@ -274,35 +269,35 @@ pub mod renderer { } } - EventResponse::default() + Response::default() } - pub fn set_clipboard_text(state: &Rc>, text: String) -> EventResponse { - let State { viewports, .. } = &mut *state.borrow_mut(); - let egui_state = viewports - .get_mut(&egui::ViewportId::ROOT) - .and_then(|viewport| viewport.egui_state.as_mut()); - match egui_state { - Some(egui_state) => { - // Requires creating an event and setting the clipboard - // here because egui_winit internally tries to manage a - // fallback clipboard for platforms not supported by the - // clipboard crates being used. - // - // This has associated behavior in the renderer to prevent - // sending 'paste events' (ctrl/cmd+V) to egui_state to - // bypass its internal clipboard handling. - egui_state - .egui_input_mut() - .events - .push(egui::Event::Paste(text.clone())); - egui_state.set_clipboard_text(text); - EventResponse { - consumed: true, - repaint: true, - } - } - _ => EventResponse::default(), + pub fn set_clipboard_text(state: &Rc>, text: String) -> Response { + let State { + viewports, focused, .. + } = &mut *state.borrow_mut(); + + let Some(viewport) = focused.and_then(|id| viewports.get_mut(&id)) else { + return Response::default(); + }; + + // Requires creating an event and setting the clipboard + // here because internally we try to manage a + // fallback clipboard for platforms not supported by the current + // clipboard backends. + // + // This has associated behavior in the renderer to prevent + // sending 'paste events' (ctrl/cmd+V) to bypass its internal + // clipboard handling. + viewport + .raw_input + .events + .push(egui::Event::Paste(text.clone())); + viewport.clipboard.set(text); + + Response { + consumed: true, + repaint: true, } } @@ -310,63 +305,96 @@ pub mod renderer { ctx: &egui::Context, state: &Rc>, gui: &Rc>, - ) -> EventResponse { + ) -> Response { #[cfg(feature = "profiling")] puffin::profile_function!(); - let raw_input = { - let State { viewports, .. } = &mut *state.borrow_mut(); + // For the purposes of processing inputs, we don't need or care about gamepad or cfg state + gui.borrow_mut() + .prepare(&Default::default(), &Default::default()); + + let (viewport_ui_cb, raw_input) = { + let State { + viewports, + start_time, + focused, + .. + } = &mut *state.borrow_mut(); - let Some(viewport) = viewports.get_mut(&egui::ViewportId::ROOT) else { - return EventResponse::default(); + let Some(viewport) = focused.and_then(|id| viewports.get_mut(&id)) else { + return Response::default(); }; let Some(window) = &viewport.window else { - return EventResponse::default(); + return Response::default(); }; - if !window.has_focus() { - return EventResponse::default(); + if viewport.occluded { + return Response::default(); } - let Some(egui_state) = viewport.egui_state.as_mut() else { - return EventResponse::default(); - }; - egui_state.take_egui_input(window) + + Viewport::update_info(&mut viewport.info, ctx, window); + + let viewport_ui_cb = viewport.viewport_ui_cb.clone(); + + // On Windows, a minimized window will have 0 width and height. + // See: https://github.com/rust-windowing/winit/issues/208 + // This solves an issue where egui window positions would be changed when minimizing on Windows. + let screen_size_in_pixels = gui::lib::screen_size_in_pixels(window); + let screen_size_in_points = + screen_size_in_pixels / gui::lib::pixels_per_point(ctx, window); + + let mut raw_input = viewport.raw_input.take(); + raw_input.time = Some(start_time.elapsed().as_secs_f64()); + raw_input.screen_rect = (screen_size_in_points.x > 0.0 + && screen_size_in_points.y > 0.0) + .then(|| egui::Rect::from_min_size(egui::Pos2::ZERO, screen_size_in_points)); + raw_input.viewport_id = viewport.ids.this; + raw_input.viewports = viewports + .iter() + .map(|(id, viewport)| (*id, viewport.info.clone())) + .collect(); + + (viewport_ui_cb, raw_input.take()) }; - let mut output = ctx.run(raw_input, |ctx| { - gui.borrow_mut().ui(ctx, None); + let mut output = ctx.run(raw_input, |ctx| match viewport_ui_cb { + Some(viewport_ui_cb) => viewport_ui_cb(ctx), + None => gui.borrow_mut().ui(ctx, None), }); - let State { viewports, .. } = &mut *state.borrow_mut(); + let State { + viewports, focused, .. + } = &mut *state.borrow_mut(); - if let Some(viewport) = viewports.get_mut(&egui::ViewportId::ROOT) { - viewport.info.events.clear(); + let Some(viewport) = focused.and_then(|id| viewports.get_mut(&id)) else { + return Response::default(); + }; - let copied_text = std::mem::take(&mut output.platform_output.copied_text); - if !copied_text.is_empty() { - if let Some(clipboard) = - web_sys::window().and_then(|window| window.navigator().clipboard()) - { - let promise = clipboard.write_text(&copied_text); - let future = JsFuture::from(promise); - let future = async move { - if let Err(err) = future.await { - tracing::error!( - "Cut/Copy failed: {}", - err.as_string().unwrap_or_else(|| format!("{err:#?}")) - ); - } - }; - thread::spawn(future); - } - } + viewport.info.events.clear(); - return EventResponse { - consumed: true, - repaint: true, - }; - }; + let copied_text = std::mem::take(&mut output.platform_output.copied_text); + tracing::warn!("Copied text: {copied_text}"); + if !copied_text.is_empty() { + if let Some(clipboard) = + web_sys::window().and_then(|window| window.navigator().clipboard()) + { + let promise = clipboard.write_text(&copied_text); + let future = JsFuture::from(promise); + let future = async move { + if let Err(err) = future.await { + tracing::error!( + "Cut/Copy failed: {}", + err.as_string().unwrap_or_else(|| format!("{err:#?}")) + ); + } + }; + thread::spawn(future); + } + } - EventResponse::default() + Response { + consumed: true, + repaint: true, + } } } @@ -819,7 +847,7 @@ pub fn download_save_states() -> anyhow::Result<()> { Ok(()) } -impl BuilderExt for WindowBuilder { +impl BuilderExt for WindowAttributes { /// Sets platform-specific window options. fn with_platform(self, _title: &str) -> Self { // Prevent default false allows cut/copy/paste @@ -827,17 +855,6 @@ impl BuilderExt for WindowBuilder { } } -impl EventLoopExt for EventLoop { - /// Runs the event loop for the current platform. - fn run_platform(self, event_handler: F) -> anyhow::Result<()> - where - F: FnMut(Event, &EventLoopWindowTarget) + 'static, - { - self.spawn(event_handler); - Ok(()) - } -} - mod html_ids { //! HTML element IDs used to interact with the DOM.