diff --git a/Cargo.lock b/Cargo.lock index 2d1a1cb..cebff23 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -276,6 +276,24 @@ dependencies = [ "libloading", ] +[[package]] +name = "ashpd" +version = "0.8.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dd884d7c72877a94102c3715f3b1cd09ff4fac28221add3e57cfbe25c236d093" +dependencies = [ + "async-fs", + "async-net", + "enumflags2", + "futures-channel", + "futures-util", + "rand", + "serde", + "serde_repr", + "url", + "zbus", +] + [[package]] name = "assert_type_match" version = "0.1.1" @@ -297,6 +315,18 @@ dependencies = [ "futures-core", ] +[[package]] +name = "async-broadcast" +version = "0.7.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "435a87a52755b8f27fcf321ac4f04b2802e337c8c4872923137471ec39c37532" +dependencies = [ + "event-listener 5.4.1", + "event-listener-strategy", + "futures-core", + "pin-project-lite", +] + [[package]] name = "async-channel" version = "2.5.0" @@ -334,6 +364,24 @@ dependencies = [ "futures-lite", ] +[[package]] +name = "async-io" +version = "2.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "456b8a8feb6f42d237746d4b3e9a178494627745c3c56c6ea55d92ba50d026fc" +dependencies = [ + "autocfg", + "cfg-if", + "concurrent-queue", + "futures-io", + "futures-lite", + "parking", + "polling", + "rustix 1.1.4", + "slab", + "windows-sys 0.61.2", +] + [[package]] name = "async-lock" version = "3.4.2" @@ -345,6 +393,64 @@ dependencies = [ "pin-project-lite", ] +[[package]] +name = "async-net" +version = "2.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b948000fad4873c1c9339d60f2623323a0cfd3816e5181033c6a5cb68b2accf7" +dependencies = [ + "async-io", + "blocking", + "futures-lite", +] + +[[package]] +name = "async-process" +version = "2.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fc50921ec0055cdd8a16de48773bfeec5c972598674347252c0399676be7da75" +dependencies = [ + "async-channel", + "async-io", + "async-lock", + "async-signal", + "async-task", + "blocking", + "cfg-if", + "event-listener 5.4.1", + "futures-lite", + "rustix 1.1.4", +] + +[[package]] +name = "async-recursion" +version = "1.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3b43422f69d8ff38f95f1b2bb76517c91589a924d1559a0e935d7c8ce0274c11" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.117", +] + +[[package]] +name = "async-signal" +version = "0.2.13" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "43c070bbf59cd3570b6b2dd54cd772527c7c3620fce8be898406dd3ed6adc64c" +dependencies = [ + "async-io", + "async-lock", + "atomic-waker", + "cfg-if", + "futures-core", + "futures-io", + "rustix 1.1.4", + "signal-hook-registry", + "slab", + "windows-sys 0.61.2", +] + [[package]] name = "async-task" version = "4.7.1" @@ -438,7 +544,7 @@ version = "0.15.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "73fd901b3be016088c4dda2f628bda96b7cb578b9bc8ae684bbf30bec0a9483e" dependencies = [ - "async-broadcast", + "async-broadcast 0.5.1", "async-fs", "async-lock", "atomicow", @@ -719,6 +825,8 @@ dependencies = [ "derive_more", "futures-lite", "image", + "ktx2", + "ruzstd", "serde", "wgpu", ] @@ -952,6 +1060,7 @@ dependencies = [ "futures-lite", "image", "js-sys", + "ktx2", "naga", "naga_oil", "nonmax", @@ -1711,7 +1820,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e0b1fab2ae45819af2d0731d60f2afe17227ebb1a1538a236da84c93e9a60162" dependencies = [ "dispatch2", - "nix", + "nix 0.31.2", "windows-sys 0.61.2", ] @@ -1907,6 +2016,33 @@ dependencies = [ "cfg-if", ] +[[package]] +name = "endi" +version = "1.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "66b7e2430c6dff6a955451e2cfc438f09cea1965a9d6f87f7e3b90decc014099" + +[[package]] +name = "enumflags2" +version = "0.7.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1027f7680c853e056ebcec683615fb6fbbc07dbaa13b4d5d9442b146ded4ecef" +dependencies = [ + "enumflags2_derive", + "serde", +] + +[[package]] +name = "enumflags2_derive" +version = "0.7.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "67c78a4d8fdf9953a5c9d458f9efe940fd97a0cab0941c075a813ac594733827" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.117", +] + [[package]] name = "epaint" version = "0.29.1" @@ -2389,6 +2525,12 @@ version = "0.5.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "fc0fef456e4baa96da950455cd02c081ca953b141298e41db3fc7e36b1da849c" +[[package]] +name = "hex" +version = "0.4.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7f24254aa9a54b5c858eaee2f5bccdb46aaf0e486a595ed5fd8f86ba55232a70" + [[package]] name = "hexasphere" version = "15.1.0" @@ -2688,6 +2830,15 @@ version = "3.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e2db585e1d738fc771bf08a151420d3ed193d9d895a36df7f6f8a9456b911ddc" +[[package]] +name = "ktx2" +version = "0.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "87d65e08a9ec02e409d27a0139eaa6b9756b4d81fe7cde71f6941a83730ce838" +dependencies = [ + "bitflags 1.3.2", +] + [[package]] name = "lazy_static" version = "1.5.0" @@ -2773,7 +2924,7 @@ version = "0.11.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "373f5eceeeab7925e0c1098212f2fbc4d416adec9d35051a6ab251e824c1854a" dependencies = [ - "twox-hash", + "twox-hash 2.1.2", ] [[package]] @@ -2820,6 +2971,15 @@ dependencies = [ "libc", ] +[[package]] +name = "memoffset" +version = "0.9.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "488016bfae457b036d996092f6cb448677611ce4449e970ceaf42695203f218a" +dependencies = [ + "autocfg", +] + [[package]] name = "metal" version = "0.29.0" @@ -2953,6 +3113,19 @@ dependencies = [ "jni-sys 0.3.1", ] +[[package]] +name = "nix" +version = "0.29.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "71e2746dc3a24dd78b3cfcb7be93368c6de9963d30f43a6a73998a9cf4b17b46" +dependencies = [ + "bitflags 2.11.0", + "cfg-if", + "cfg_aliases 0.2.1", + "libc", + "memoffset", +] + [[package]] name = "nix" version = "0.31.2" @@ -3043,6 +3216,17 @@ dependencies = [ "malloc_buf", ] +[[package]] +name = "objc-foundation" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1add1b659e36c9607c7aab864a76c7a4c2760cd0cd2e120f3fb8b952c7e22bf9" +dependencies = [ + "block", + "objc", + "objc_id", +] + [[package]] name = "objc-sys" version = "0.3.5" @@ -3313,6 +3497,15 @@ dependencies = [ "objc2-foundation 0.2.2", ] +[[package]] +name = "objc_id" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c92d4ddb4bd7b50d730c215ff871754d0da6b2178849f8a2a2ab69712d0c073b" +dependencies = [ + "objc", +] + [[package]] name = "offset-allocator" version = "0.2.0" @@ -3345,6 +3538,16 @@ dependencies = [ "libredox", ] +[[package]] +name = "ordered-stream" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9aa2b01e1d916879f73a53d01d1d6cee68adbb31d6d9177a8cfce093cced1d50" +dependencies = [ + "futures-core", + "pin-project-lite", +] + [[package]] name = "owned_ttf_parser" version = "0.25.1" @@ -3487,6 +3690,12 @@ dependencies = [ "windows-sys 0.61.2", ] +[[package]] +name = "pollster" +version = "0.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "22686f4785f02a4fcc856d3b3bb19bf6c8160d103f7a99cc258bddd0251dc7f2" + [[package]] name = "pori" version = "0.0.0" @@ -3754,6 +3963,29 @@ version = "1.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "19b30a45b0cd0bcca8037f3d0dc3421eaf95327a17cad11964fb8179b4fc4832" +[[package]] +name = "rfd" +version = "0.14.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "25a73a7337fc24366edfca76ec521f51877b114e42dab584008209cca6719251" +dependencies = [ + "ashpd", + "block", + "dispatch", + "js-sys", + "log", + "objc", + "objc-foundation", + "objc_id", + "pollster", + "raw-window-handle", + "urlencoding", + "wasm-bindgen", + "wasm-bindgen-futures", + "web-sys", + "windows-sys 0.48.0", +] + [[package]] name = "ron" version = "0.8.1" @@ -3813,6 +4045,15 @@ version = "1.0.22" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b39cdef0fa800fc44525c84ccb54a029961a8215f9619753635a9c0d2538d46d" +[[package]] +name = "ruzstd" +version = "0.7.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fad02996bfc73da3e301efe90b1837be9ed8f4a462b6ed410aa35d00381de89f" +dependencies = [ + "twox-hash 1.6.3", +] + [[package]] name = "same-file" version = "1.0.6" @@ -3911,6 +4152,28 @@ dependencies = [ "zmij", ] +[[package]] +name = "serde_repr" +version = "0.1.20" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "175ee3e80ae9982737ca543e96133087cbd9a485eecc3bc4de9c1a37b47ea59c" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.117", +] + +[[package]] +name = "sha1" +version = "0.10.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e3bf829a2d51ab4a5ddf1352d8470c140cadc8301b2ae1789db023f01cedd6ba" +dependencies = [ + "cfg-if", + "cpufeatures", + "digest", +] + [[package]] name = "sha3" version = "0.10.8" @@ -4095,6 +4358,7 @@ version = "0.1.0" dependencies = [ "anyhow", "binrw", + "flate2", "futures", "serde", "serde_json", @@ -4110,6 +4374,8 @@ version = "0.1.0" dependencies = [ "bevy", "bevy_egui", + "futures", + "rfd", "sylpheed-formats", "thiserror 2.0.18", "tracing", @@ -4149,6 +4415,19 @@ dependencies = [ "syn 2.0.117", ] +[[package]] +name = "tempfile" +version = "3.27.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "32497e9a4c7b38532efcdebeef879707aa9f794296a4f0244f6f69e9bc8574bd" +dependencies = [ + "fastrand", + "getrandom 0.3.4", + "once_cell", + "rustix 1.1.4", + "windows-sys 0.61.2", +] + [[package]] name = "termcolor" version = "1.4.1" @@ -4448,6 +4727,16 @@ version = "0.25.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d2df906b07856748fa3f6e0ad0cbaa047052d4a7dd609e231c4f72cee8c36f31" +[[package]] +name = "twox-hash" +version = "1.6.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "97fee6b57c6a41524a810daee9286c02d7752c4253064d0b05472833a438f675" +dependencies = [ + "cfg-if", + "static_assertions", +] + [[package]] name = "twox-hash" version = "2.1.2" @@ -4472,6 +4761,17 @@ version = "1.14.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "f8c1ae7cc0fdb8b842d65d127cb981574b0d2b249b74d1c7a2986863dc134f71" +[[package]] +name = "uds_windows" +version = "1.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f2f6fb2847f6742cd76af783a2a2c49e9375d0a111c7bef6f71cd9e738c72d6e" +dependencies = [ + "memoffset", + "tempfile", + "windows-sys 0.61.2", +] + [[package]] name = "unicode-ident" version = "1.0.24" @@ -4512,8 +4812,15 @@ dependencies = [ "idna", "percent-encoding", "serde", + "serde_derive", ] +[[package]] +name = "urlencoding" +version = "2.1.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "daf8dba3b7eb870caf1ddeed7bc9d2a049f3cfdfae7cb521b087cc33ae4c49da" + [[package]] name = "utf8_iter" version = "1.0.4" @@ -4982,6 +5289,15 @@ dependencies = [ "windows-targets 0.52.6", ] +[[package]] +name = "windows-sys" +version = "0.48.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "677d2418bec65e3338edb076e806bc1ec15693c5d0104683f2efe857f61056a9" +dependencies = [ + "windows-targets 0.48.5", +] + [[package]] name = "windows-sys" version = "0.52.0" @@ -5018,6 +5334,21 @@ dependencies = [ "windows-link", ] +[[package]] +name = "windows-targets" +version = "0.48.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9a2fa6e2155d7247be68c096456083145c183cbbbc2764150dda45a87197940c" +dependencies = [ + "windows_aarch64_gnullvm 0.48.5", + "windows_aarch64_msvc 0.48.5", + "windows_i686_gnu 0.48.5", + "windows_i686_msvc 0.48.5", + "windows_x86_64_gnu 0.48.5", + "windows_x86_64_gnullvm 0.48.5", + "windows_x86_64_msvc 0.48.5", +] + [[package]] name = "windows-targets" version = "0.52.6" @@ -5051,6 +5382,12 @@ dependencies = [ "windows_x86_64_msvc 0.53.1", ] +[[package]] +name = "windows_aarch64_gnullvm" +version = "0.48.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2b38e32f0abccf9987a4e3079dfb67dcd799fb61361e53e2882c3cbaf0d905d8" + [[package]] name = "windows_aarch64_gnullvm" version = "0.52.6" @@ -5063,6 +5400,12 @@ version = "0.53.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "a9d8416fa8b42f5c947f8482c43e7d89e73a173cead56d044f6a56104a6d1b53" +[[package]] +name = "windows_aarch64_msvc" +version = "0.48.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dc35310971f3b2dbbf3f0690a219f40e2d9afcf64f9ab7cc1be722937c26b4bc" + [[package]] name = "windows_aarch64_msvc" version = "0.52.6" @@ -5075,6 +5418,12 @@ version = "0.53.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b9d782e804c2f632e395708e99a94275910eb9100b2114651e04744e9b125006" +[[package]] +name = "windows_i686_gnu" +version = "0.48.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a75915e7def60c94dcef72200b9a8e58e5091744960da64ec734a6c6e9b3743e" + [[package]] name = "windows_i686_gnu" version = "0.52.6" @@ -5099,6 +5448,12 @@ version = "0.53.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "fa7359d10048f68ab8b09fa71c3daccfb0e9b559aed648a8f95469c27057180c" +[[package]] +name = "windows_i686_msvc" +version = "0.48.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8f55c233f70c4b27f66c523580f78f1004e8b5a8b659e05a4eb49d4166cca406" + [[package]] name = "windows_i686_msvc" version = "0.52.6" @@ -5111,6 +5466,12 @@ version = "0.53.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "1e7ac75179f18232fe9c285163565a57ef8d3c89254a30685b57d83a38d326c2" +[[package]] +name = "windows_x86_64_gnu" +version = "0.48.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "53d40abd2583d23e4718fddf1ebec84dbff8381c07cae67ff7768bbf19c6718e" + [[package]] name = "windows_x86_64_gnu" version = "0.52.6" @@ -5123,6 +5484,12 @@ version = "0.53.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9c3842cdd74a865a8066ab39c8a7a473c0778a3f29370b5fd6b4b9aa7df4a499" +[[package]] +name = "windows_x86_64_gnullvm" +version = "0.48.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0b7b52767868a23d5bab768e390dc5f5c55825b6d30b86c844ff2dc7414044cc" + [[package]] name = "windows_x86_64_gnullvm" version = "0.52.6" @@ -5135,6 +5502,12 @@ version = "0.53.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "0ffa179e2d07eee8ad8f57493436566c7cc30ac536a3379fdf008f47f6bb7ae1" +[[package]] +name = "windows_x86_64_msvc" +version = "0.48.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ed94fce61571a4006852b7389a063ab983c02eb1bb37b47f8272ce92d06d9538" + [[package]] name = "windows_x86_64_msvc" version = "0.52.6" @@ -5267,6 +5640,16 @@ version = "0.3.10" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "bec9e4a500ca8864c5b47b8b482a73d62e4237670e5b5f1d6b9e3cae50f28f2b" +[[package]] +name = "xdg-home" +version = "1.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ec1cdab258fb55c0da61328dc52c8764709b249011b2cad0454c72f0bf10a1f6" +dependencies = [ + "libc", + "windows-sys 0.59.0", +] + [[package]] name = "xdvdfs" version = "0.8.3" @@ -5335,6 +5718,68 @@ dependencies = [ "synstructure", ] +[[package]] +name = "zbus" +version = "4.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bb97012beadd29e654708a0fdb4c84bc046f537aecfde2c3ee0a9e4b4d48c725" +dependencies = [ + "async-broadcast 0.7.2", + "async-executor", + "async-fs", + "async-io", + "async-lock", + "async-process", + "async-recursion", + "async-task", + "async-trait", + "blocking", + "enumflags2", + "event-listener 5.4.1", + "futures-core", + "futures-sink", + "futures-util", + "hex", + "nix 0.29.0", + "ordered-stream", + "rand", + "serde", + "serde_repr", + "sha1", + "static_assertions", + "tracing", + "uds_windows", + "windows-sys 0.52.0", + "xdg-home", + "zbus_macros", + "zbus_names", + "zvariant", +] + +[[package]] +name = "zbus_macros" +version = "4.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "267db9407081e90bbfa46d841d3cbc60f59c0351838c4bc65199ecd79ab1983e" +dependencies = [ + "proc-macro-crate", + "proc-macro2", + "quote", + "syn 2.0.117", + "zvariant_utils", +] + +[[package]] +name = "zbus_names" +version = "3.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4b9b1fef7d021261cc16cba64c351d291b715febe0fa10dc3a443ac5a5022e6c" +dependencies = [ + "serde", + "static_assertions", + "zvariant", +] + [[package]] name = "zerocopy" version = "0.8.47" @@ -5429,3 +5874,41 @@ checksum = "0b7a1c0af6e5d8d1363f4994b7a091ccf963d8b694f7da5b0b9cceb82da2c0a6" dependencies = [ "zune-core", ] + +[[package]] +name = "zvariant" +version = "4.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2084290ab9a1c471c38fc524945837734fbf124487e105daec2bb57fd48c81fe" +dependencies = [ + "endi", + "enumflags2", + "serde", + "static_assertions", + "url", + "zvariant_derive", +] + +[[package]] +name = "zvariant_derive" +version = "4.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "73e2ba546bda683a90652bac4a279bc146adad1386f25379cf73200d2002c449" +dependencies = [ + "proc-macro-crate", + "proc-macro2", + "quote", + "syn 2.0.117", + "zvariant_utils", +] + +[[package]] +name = "zvariant_utils" +version = "2.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c51bcff7cc3dbb5055396bcf774748c3dab426b4b8659046963523cee4808340" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.117", +] diff --git a/Cargo.toml b/Cargo.toml index 0752a3f..4e9c614 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -39,6 +39,9 @@ anyhow = "1" tracing = "0.1" tracing-subscriber = { version = "0.3", features = ["env-filter"] } +# ── Native file dialog ──────────────────────────────────────────────────────── +rfd = "0.14" + # ── CLI ─────────────────────────────────────────────────────────────────────── clap = { version = "4", features = ["derive"] } diff --git a/crates/sylpheed-viewer/Cargo.toml b/crates/sylpheed-viewer/Cargo.toml index bab10eb..e97637d 100644 --- a/crates/sylpheed-viewer/Cargo.toml +++ b/crates/sylpheed-viewer/Cargo.toml @@ -31,11 +31,14 @@ bevy = { workspace = true, features = [ "bevy_winit", "multi_threaded", "png", + # Required for TonyMcMapFace tonemapping (default Camera3d tonemapper) + "tonemapping_luts", # Linux display backends "x11", "wayland", ] } bevy_egui = { workspace = true } +futures = { workspace = true } thiserror = { workspace = true } tracing = { workspace = true } @@ -46,3 +49,4 @@ dev = ["bevy/dynamic_linking"] [target.'cfg(not(target_arch = "wasm32"))'.dependencies] tracing-subscriber = { workspace = true } +rfd = { workspace = true } diff --git a/crates/sylpheed-viewer/src/asset_loader.rs b/crates/sylpheed-viewer/src/asset_loader.rs index 8131dbe..e13f91f 100644 --- a/crates/sylpheed-viewer/src/asset_loader.rs +++ b/crates/sylpheed-viewer/src/asset_loader.rs @@ -12,10 +12,11 @@ //! 4. Bevy uploads it to the GPU automatically use bevy::asset::io::Reader; -use bevy::asset::{AssetLoader, AsyncReadExt, LoadContext}; +use bevy::asset::{AssetLoader, LoadContext}; +use bevy::image::ImageSampler; use bevy::prelude::*; use bevy::render::render_asset::RenderAssetUsages; -use bevy::render::render_resource::{Extent3d, TextureDimension, TextureFormat}; +use bevy::render::render_resource::{Extent3d, TextureDescriptor, TextureDimension, TextureFormat, TextureUsages}; use sylpheed_formats::texture::{X360Texture, X360TextureFormat}; // ── Plugin ──────────────────────────────────────────────────────────────────── @@ -86,24 +87,32 @@ impl AssetLoader for Xpr2TextureLoader { /// Convert a decoded `X360Texture` into a Bevy-compatible `Image`. /// -/// Maps Xbox 360 D3DFORMAT codes to `wgpu::TextureFormat` values. -/// Bevy uses wgpu internally, so this mapping is direct. +/// Constructs the `Image` struct directly rather than using `Image::new()`, +/// because `Image::new()` calls `pixel_size()` which panics for BCn +/// block-compressed formats. pub fn x360_texture_to_bevy_image(tex: X360Texture) -> Result { - let bevy_format = x360_format_to_wgpu(&tex.format)?; + let format = x360_format_to_wgpu(&tex.format)?; - Ok(Image::new( - Extent3d { - width: tex.width, - height: tex.height, - depth_or_array_layers: 1, + Ok(Image { + data: tex.data, + texture_descriptor: TextureDescriptor { + label: None, + size: Extent3d { + width: tex.width, + height: tex.height, + depth_or_array_layers: 1, + }, + mip_level_count: 1, + sample_count: 1, + dimension: TextureDimension::D2, + format, + usage: TextureUsages::TEXTURE_BINDING | TextureUsages::COPY_DST, + view_formats: &[], }, - TextureDimension::D2, - tex.data, - bevy_format, - // Make the texture available on both CPU and GPU - // (RenderOnly would be more efficient once RE is complete) - RenderAssetUsages::MAIN_WORLD | RenderAssetUsages::RENDER_WORLD, - )) + sampler: ImageSampler::Default, + texture_view_descriptor: None, + asset_usage: RenderAssetUsages::MAIN_WORLD | RenderAssetUsages::RENDER_WORLD, + }) } /// Map an `X360TextureFormat` to the corresponding `wgpu::TextureFormat`. @@ -119,14 +128,12 @@ fn x360_format_to_wgpu(format: &X360TextureFormat) -> Result TextureFormat::Bc2RgbaUnormSrgb, // DXT5 / BC3 X360TextureFormat::Dxt5 => TextureFormat::Bc3RgbaUnormSrgb, - // DXN / BC5 — normal maps (RG, not sRGB) + // DXN / BC5 — two-channel normal maps (RG, not sRGB) X360TextureFormat::Dxn => TextureFormat::Bc5RgUnorm, - // Uncompressed ARGB — note Xbox 360 is BGRA order (big-endian) - // We may need to swizzle R and B channels + // DXT5A / BC4 — single-channel (gloss, specular, luminance maps) + X360TextureFormat::Dxt5A => TextureFormat::Bc4RUnorm, + // Uncompressed ARGB — Xbox 360 stores as BGRA in memory X360TextureFormat::A8R8G8B8 | X360TextureFormat::X8R8G8B8 => { - // Xbox 360 stores as ARGB big-endian (BGRA in memory) - // wgpu uses Rgba8UnormSrgb — swizzle may be needed - // TODO: verify channel order against actual game textures TextureFormat::Bgra8UnormSrgb } }) diff --git a/crates/sylpheed-viewer/src/iso_loader.rs b/crates/sylpheed-viewer/src/iso_loader.rs new file mode 100644 index 0000000..81f6c6f --- /dev/null +++ b/crates/sylpheed-viewer/src/iso_loader.rs @@ -0,0 +1,458 @@ +//! ISO / extracted-directory loading pipeline for the viewer. +//! +//! Bridges the async `XisoReader` API (and synchronous `GameAssets`) with +//! Bevy's ECS via background threads and `std::sync::mpsc` channels polled +//! each frame — no blocking on the main thread. +//! +//! ## Data flow (native) +//! +//! ```text +//! UI click → Event → handle_open_iso / handle_open_dir / handle_file_selected +//! └─ std::thread ─→ mpsc::Sender +//! ↓ (next frame) +//! poll_loader_channel → FileBrowserState / PendingFileBytes +//! ↓ +//! apply_loaded_texture → TexturePreview (Image + egui TextureId) +//! ↓ +//! draw_viewer_ui → renders preview panel +//! ``` + +#[cfg(not(target_arch = "wasm32"))] +use std::sync::{mpsc, Mutex}; + +use std::path::PathBuf; + +use bevy::prelude::*; +use bevy_egui::egui; + +use sylpheed_formats::vfs::FileFormat; + +use crate::ui::FileBrowserState; + +// ── Events ──────────────────────────────────────────────────────────────────── + +/// Fired by the "Open ISO disc image…" menu item. +#[derive(Event, Default)] +pub struct RequestOpenIso; + +/// Fired by the "Open extracted folder…" menu item. +#[derive(Event, Default)] +pub struct RequestOpenDir; + +/// Fired when the user clicks a file in the browser panel. +#[derive(Event)] +pub struct FileSelected(pub String); + +// ── Resources (platform-agnostic) ──────────────────────────────────────────── + +/// Which kind of source is currently open. +#[derive(Default, Clone)] +pub enum SourceKind { + #[default] + None, + #[cfg(not(target_arch = "wasm32"))] + Iso(PathBuf), + #[cfg(not(target_arch = "wasm32"))] + Directory(PathBuf), +} + +/// State of the currently-open file source (ISO or extracted folder). +#[derive(Resource, Default)] +pub struct IsoState { + /// Short description shown in the status bar (filename or directory path). + pub source_label: Option, + /// True while a background thread is loading. + pub loading: bool, + /// The most recent error message (cleared when a new operation starts). + pub error: Option, + /// Which backend to use when reading individual files. + pub source_kind: SourceKind, +} + +/// The texture currently shown in the central panel. +#[derive(Resource, Default)] +pub struct TexturePreview { + /// Bevy handle — kept alive so the GPU texture stays allocated. + pub handle: Option>, + /// egui texture ID registered via `EguiContexts::add_image`. + pub egui_id: Option, + pub width: u32, + pub height: u32, + /// Human-readable format / dimension summary (or error message). + pub format_info: String, +} + +/// Info shown for any selected file (texture or not). +#[derive(Resource, Default)] +pub struct FileInfo { + pub name: String, + pub size_bytes: usize, + pub detected_format: Option, +} + +// ── SystemSet label ─────────────────────────────────────────────────────────── + +/// `draw_viewer_ui` runs `.after(IsoLoaderSystemSet)` to see the frame's +/// final state before rendering. +#[derive(SystemSet, Debug, Clone, PartialEq, Eq, Hash)] +pub struct IsoLoaderSystemSet; + +// ── Native-only types ───────────────────────────────────────────────────────── + +/// Messages sent from background threads back to the Bevy main thread. +#[cfg(not(target_arch = "wasm32"))] +enum IsoLoaderMsg { + FilesListed { + source: SourceKind, + label: String, + files: Vec, + }, + FileLoaded { + path: String, + bytes: Vec, + }, + /// User dismissed the file dialog — not an error. + Cancelled, + Error(String), +} + +/// Channel resource. `Receiver` is `!Sync`, so we wrap it in `Mutex` +/// to satisfy Bevy's `Resource: Send + Sync` bound. +#[cfg(not(target_arch = "wasm32"))] +#[derive(Resource)] +struct IsoChannels { + sender: mpsc::Sender, + receiver: Mutex>, +} + +#[cfg(not(target_arch = "wasm32"))] +impl Default for IsoChannels { + fn default() -> Self { + let (sender, receiver) = mpsc::channel(); + Self { + sender, + receiver: Mutex::new(receiver), + } + } +} + +/// One-frame staging buffer: `poll_loader_channel` deposits raw bytes here; +/// `apply_loaded_texture` (chained after) consumes them. +#[cfg(not(target_arch = "wasm32"))] +#[derive(Resource, Default)] +struct PendingFileBytes { + ready: bool, + path: String, + bytes: Vec, +} + +// ── Plugin ──────────────────────────────────────────────────────────────────── + +pub struct IsoLoaderPlugin; + +impl Plugin for IsoLoaderPlugin { + fn build(&self, app: &mut App) { + app.add_event::() + .add_event::() + .add_event::() + .init_resource::() + .init_resource::() + .init_resource::(); + + #[cfg(not(target_arch = "wasm32"))] + { + app.init_resource::() + .init_resource::() + .add_systems( + Update, + ( + handle_open_iso, + handle_open_dir, + handle_file_selected, + poll_loader_channel, + apply_loaded_texture, + ) + .chain() + .in_set(IsoLoaderSystemSet), + ); + } + } +} + +// ── Systems (native-only) ───────────────────────────────────────────────────── + +/// Listens for `RequestOpenIso`, opens a native file-picker in a background +/// thread, then lists all files in the selected ISO. +#[cfg(not(target_arch = "wasm32"))] +fn handle_open_iso( + mut events: EventReader, + mut iso_state: ResMut, + channels: Res, +) { + let count = events.read().count(); + if count == 0 || iso_state.loading { + return; + } + + iso_state.loading = true; + iso_state.error = None; + + let sender = channels.sender.clone(); + std::thread::spawn(move || { + let path = rfd::FileDialog::new() + .set_title("Open Xbox 360 Disc Image") + .add_filter("Xbox 360 ISO", &["iso", "xiso"]) + .pick_file(); + + let Some(path) = path else { + let _ = sender.send(IsoLoaderMsg::Cancelled); + return; + }; + + let label = path.display().to_string(); + let iso_path = path.clone(); + + let result = futures::executor::block_on(async move { + let mut reader = sylpheed_formats::xiso::open_iso(&path) + .await + .map_err(|e| e.to_string())?; + let files = reader + .list_all_files() + .await + .map_err(|e| e.to_string())?; + Ok::<_, String>((iso_path, label, files)) + }); + + match result { + Ok((iso_path, label, files)) => { + let _ = sender.send(IsoLoaderMsg::FilesListed { + source: SourceKind::Iso(iso_path), + label, + files, + }); + } + Err(e) => { + let _ = sender.send(IsoLoaderMsg::Error(e)); + } + } + }); +} + +/// Listens for `RequestOpenDir`, opens a folder picker, lists files via +/// `GameAssets` (synchronous). +#[cfg(not(target_arch = "wasm32"))] +fn handle_open_dir( + mut events: EventReader, + mut iso_state: ResMut, + channels: Res, +) { + let count = events.read().count(); + if count == 0 || iso_state.loading { + return; + } + + iso_state.loading = true; + iso_state.error = None; + + let sender = channels.sender.clone(); + std::thread::spawn(move || { + let path = rfd::FileDialog::new() + .set_title("Open Extracted Game Directory") + .pick_folder(); + + let Some(path) = path else { + let _ = sender.send(IsoLoaderMsg::Cancelled); + return; + }; + + let label = path.display().to_string(); + let assets = sylpheed_formats::vfs::GameAssets::from_directory(&path); + match assets.list("") { + Ok(files) => { + let _ = sender.send(IsoLoaderMsg::FilesListed { + source: SourceKind::Directory(path), + label, + files, + }); + } + Err(e) => { + let _ = sender.send(IsoLoaderMsg::Error(e.to_string())); + } + } + }); +} + +/// Listens for `FileSelected`, reads the file from the active source, +/// sends bytes back via the channel. +#[cfg(not(target_arch = "wasm32"))] +fn handle_file_selected( + mut events: EventReader, + iso_state: Res, + channels: Res, +) { + for FileSelected(file_path) in events.read() { + let file_path = file_path.clone(); + let sender = channels.sender.clone(); + + match &iso_state.source_kind { + SourceKind::Iso(iso_path) => { + let iso_path = iso_path.clone(); + std::thread::spawn(move || { + let result = futures::executor::block_on(async { + let mut reader = + sylpheed_formats::xiso::open_iso(&iso_path) + .await + .map_err(|e| e.to_string())?; + let bytes = reader + .read_file(&file_path) + .await + .map_err(|e| e.to_string())?; + Ok::<_, String>((file_path, bytes)) + }); + + match result { + Ok((path, bytes)) => { + let _ = sender + .send(IsoLoaderMsg::FileLoaded { path, bytes }); + } + Err(e) => { + let _ = sender.send(IsoLoaderMsg::Error(e)); + } + } + }); + } + SourceKind::Directory(root) => { + let assets = + sylpheed_formats::vfs::GameAssets::from_directory(root); + match assets.read(&file_path) { + Ok(bytes) => { + let _ = sender.send(IsoLoaderMsg::FileLoaded { + path: file_path, + bytes, + }); + } + Err(e) => { + let _ = sender.send(IsoLoaderMsg::Error(e.to_string())); + } + } + } + SourceKind::None => {} + } + } +} + +/// Polls the mpsc channel, updating `IsoState`, `FileBrowserState`, and +/// `PendingFileBytes` as messages arrive. +#[cfg(not(target_arch = "wasm32"))] +fn poll_loader_channel( + channels: Res, + mut iso_state: ResMut, + mut browser: ResMut, + mut pending: ResMut, +) { + let receiver = channels.receiver.lock().unwrap(); + loop { + use std::sync::mpsc::TryRecvError; + match receiver.try_recv() { + Ok(IsoLoaderMsg::FilesListed { + source, + label, + files, + }) => { + iso_state.loading = false; + iso_state.error = None; + iso_state.source_label = Some(label); + iso_state.source_kind = source; + browser.files = files; + browser.selected = None; + browser.filter.clear(); + info!("Loaded {} files", browser.files.len()); + } + Ok(IsoLoaderMsg::FileLoaded { path, bytes }) => { + pending.path = path; + pending.bytes = bytes; + pending.ready = true; + } + Ok(IsoLoaderMsg::Cancelled) => { + iso_state.loading = false; + } + Ok(IsoLoaderMsg::Error(msg)) => { + error!("ISO loader: {}", msg); + iso_state.loading = false; + iso_state.error = Some(msg); + } + Err(TryRecvError::Empty) | Err(TryRecvError::Disconnected) => break, + } + } +} + +/// Converts pending raw bytes into a Bevy `Image`, registers it with egui, +/// and populates `TexturePreview` / `FileInfo`. +#[cfg(not(target_arch = "wasm32"))] +fn apply_loaded_texture( + mut pending: ResMut, + mut preview: ResMut, + mut file_info: ResMut, + mut images: ResMut>, + mut contexts: bevy_egui::EguiContexts, +) { + if !pending.ready { + return; + } + pending.ready = false; + + let bytes = std::mem::take(&mut pending.bytes); + let path = std::mem::take(&mut pending.path); + + let fmt = sylpheed_formats::vfs::identify_format(&bytes); + + // Always free the previous texture first (GPU memory + egui slot). + if let Some(ref handle) = preview.handle { + contexts.remove_image(handle); + images.remove(handle.id()); + } + *preview = TexturePreview::default(); + + // Populate FileInfo for the status / info panel. + file_info.name = path + .split('/') + .next_back() + .unwrap_or(&path) + .to_string(); + file_info.size_bytes = bytes.len(); + file_info.detected_format = Some(fmt); + + if fmt != FileFormat::Xpr2Texture { + return; // Not a texture — FileInfo is sufficient + } + + let tex = match sylpheed_formats::X360Texture::from_xpr2(&bytes) { + Ok(t) => t, + Err(e) => { + preview.format_info = format!("XPR2 parse failed: {e}"); + return; + } + }; + + let w = tex.width; + let h = tex.height; + let mips = tex.mip_levels; + let fmt_str = format!("{:?} {}×{} {} mip(s)", tex.format, w, h, mips); + + let image = match crate::asset_loader::x360_texture_to_bevy_image(tex) { + Ok(img) => img, + Err(e) => { + preview.format_info = format!("Bevy image conversion failed: {e}"); + return; + } + }; + + let handle = images.add(image); + let egui_id = contexts.add_image(handle.clone_weak()); + + preview.handle = Some(handle); + preview.egui_id = Some(egui_id); + preview.width = w; + preview.height = h; + preview.format_info = fmt_str; +} diff --git a/crates/sylpheed-viewer/src/lib.rs b/crates/sylpheed-viewer/src/lib.rs index e5b558f..b6e4008 100644 --- a/crates/sylpheed-viewer/src/lib.rs +++ b/crates/sylpheed-viewer/src/lib.rs @@ -20,6 +20,7 @@ use bevy_egui::EguiPlugin; pub mod asset_loader; pub mod camera; +pub mod iso_loader; pub mod ui; // ── Application state ──────────────────────────────────────────────────────── @@ -75,6 +76,7 @@ pub fn run() { app.add_plugins(EguiPlugin); app.add_plugins(asset_loader::SylpheedAssetPlugin); + app.add_plugins(iso_loader::IsoLoaderPlugin); app.add_plugins(camera::OrbitCameraPlugin); app.add_plugins(ui::ViewerUiPlugin); diff --git a/crates/sylpheed-viewer/src/ui.rs b/crates/sylpheed-viewer/src/ui.rs index 8dfe157..a4fdccb 100644 --- a/crates/sylpheed-viewer/src/ui.rs +++ b/crates/sylpheed-viewer/src/ui.rs @@ -1,21 +1,27 @@ //! egui-based debug UI for the asset viewer. //! //! Provides panels for: -//! - File browser (list extracted game files) +//! - File browser (list ISO / extracted game files, filter by name) //! - Texture inspector (preview + format info) -//! - Mesh inspector (vertex/index counts, materials) +//! - File info (size, detected format for non-texture files) //! - RE notes (track discoveries during reverse engineering) use bevy::prelude::*; use bevy_egui::{egui, EguiContexts}; + +use crate::iso_loader::{ + FileInfo, FileSelected, IsoState, RequestOpenDir, RequestOpenIso, TexturePreview, + IsoLoaderSystemSet, +}; use crate::{ViewerState, ViewMode}; pub struct ViewerUiPlugin; impl Plugin for ViewerUiPlugin { fn build(&self, app: &mut App) { - app.add_systems(Update, draw_viewer_ui); app.insert_resource(FileBrowserState::default()); + // Run after the iso_loader chain so we see the frame's final state. + app.add_systems(Update, draw_viewer_ui.after(IsoLoaderSystemSet)); } } @@ -31,7 +37,12 @@ fn draw_viewer_ui( mut contexts: EguiContexts, mut viewer: ResMut, mut browser: ResMut, - asset_server: Res, + iso_state: Res, + preview: Res, + file_info: Res, + mut open_iso_events: EventWriter, + mut open_dir_events: EventWriter, + mut file_selected_events: EventWriter, ) { let ctx = contexts.ctx_mut(); @@ -39,26 +50,45 @@ fn draw_viewer_ui( egui::TopBottomPanel::top("menu_bar").show(ctx, |ui| { egui::menu::bar(ui, |ui| { ui.menu_button("File", |ui| { - if ui.button("Open extracted directory...").clicked() { - // TODO: file dialog (rfd crate on native, not available on WASM) - info!("TODO: open directory dialog"); - ui.close_menu(); - } - ui.separator(); #[cfg(not(target_arch = "wasm32"))] - if ui.button("Quit").clicked() { - std::process::exit(0); + { + if ui.button("Open ISO disc image…").clicked() { + open_iso_events.send_default(); + ui.close_menu(); + } + if ui.button("Open extracted folder…").clicked() { + open_dir_events.send_default(); + ui.close_menu(); + } + ui.separator(); + if ui.button("Quit").clicked() { + std::process::exit(0); + } } + #[cfg(target_arch = "wasm32")] + ui.colored_label( + egui::Color32::GRAY, + "File loading not available in browser", + ); }); ui.menu_button("View", |ui| { - if ui.selectable_label(viewer.current_mode == ViewMode::Textures, "Textures").clicked() { + if ui + .selectable_label(viewer.current_mode == ViewMode::Textures, "Textures") + .clicked() + { viewer.current_mode = ViewMode::Textures; } - if ui.selectable_label(viewer.current_mode == ViewMode::Mesh, "Meshes").clicked() { + if ui + .selectable_label(viewer.current_mode == ViewMode::Mesh, "Meshes") + .clicked() + { viewer.current_mode = ViewMode::Mesh; } - if ui.selectable_label(viewer.current_mode == ViewMode::Audio, "Audio").clicked() { + if ui + .selectable_label(viewer.current_mode == ViewMode::Audio, "Audio") + .clicked() + { viewer.current_mode = ViewMode::Audio; } ui.separator(); @@ -66,7 +96,7 @@ fn draw_viewer_ui( }); ui.menu_button("Help", |ui| { - if ui.button("Controls...").clicked() { + if ui.button("Controls…").clicked() { // TODO: controls popup } if ui.button("About").clicked() { @@ -93,29 +123,39 @@ fn draw_viewer_ui( // File list egui::ScrollArea::vertical().show(ui, |ui| { - if browser.files.is_empty() { + if iso_state.loading { + ui.spinner(); + ui.label("Loading…"); + } else if let Some(err) = &iso_state.error { + ui.colored_label( + egui::Color32::RED, + format!("⚠ Error:\n{}", err), + ); + } else if browser.files.is_empty() { ui.colored_label( egui::Color32::YELLOW, - "⚠ No files loaded.\nExtract your ISO first:\n\ - xdvdfs unpack game.iso ./assets/", + "No files loaded.\nUse File → Open ISO disc image\n\ + or File → Open extracted folder.", ); } else { let filter = browser.filter.to_lowercase(); - // Collect owned strings so we can later mutate `browser.selected` - let filtered: Vec<(usize, String)> = browser.files + let filtered: Vec<(usize, String)> = browser + .files .iter() .enumerate() - .filter(|(_, f)| filter.is_empty() || f.to_lowercase().contains(&filter)) + .filter(|(_, f)| { + filter.is_empty() || f.to_lowercase().contains(&filter) + }) .map(|(i, f)| (i, f.clone())) .collect(); for (idx, file) in &filtered { let is_selected = browser.selected == Some(*idx); - let label = egui::SelectableLabel::new(is_selected, file.as_str()); + let label = + egui::SelectableLabel::new(is_selected, file.as_str()); if ui.add(label).clicked() { browser.selected = Some(*idx); - info!("Selected file: {}", file); - // TODO: load the selected file into the viewer + file_selected_events.send(FileSelected(file.clone())); } } } @@ -125,31 +165,84 @@ fn draw_viewer_ui( // ── Bottom panel: status bar ────────────────────────────────────────── egui::TopBottomPanel::bottom("status_bar").show(ctx, |ui| { ui.horizontal(|ui| { + if let Some(label) = &iso_state.source_label { + ui.label(label); + ui.separator(); + } match viewer.current_mode { ViewMode::Textures => ui.label("Mode: Texture Viewer"), - ViewMode::Mesh => ui.label("Mode: Mesh Viewer"), - ViewMode::Audio => ui.label("Mode: Audio Viewer"), + ViewMode::Mesh => ui.label("Mode: Mesh Viewer"), + ViewMode::Audio => ui.label("Mode: Audio Viewer"), }; ui.separator(); - ui.label(format!("{} files loaded", browser.files.len())); + ui.label(format!("{} files", browser.files.len())); ui.separator(); - ui.label("LMB drag: orbit | RMB drag: pan | Scroll: zoom | R: reset"); + ui.label("LMB: orbit | RMB: pan | Scroll: zoom | R: reset"); }); }); - // ── Central area: RE notes (shown when no asset is selected) ────────── - if browser.selected.is_none() { + // ── Central area ────────────────────────────────────────────────────── + if browser.selected.is_some() { + egui::CentralPanel::default().show(ctx, |ui| { + if let Some(egui_id) = preview.egui_id { + // Texture preview + ui.heading(&file_info.name); + ui.label(&preview.format_info); + if !preview.format_info.is_empty() && preview.format_info.contains("failed") { + ui.colored_label(egui::Color32::YELLOW, &preview.format_info); + } + ui.separator(); + + // Scale to fit, preserving aspect ratio, never upscale + let avail = ui.available_size(); + let scale = if preview.width == 0 || preview.height == 0 { + 1.0_f32 + } else { + (avail.x / preview.width as f32) + .min(avail.y / preview.height as f32) + .min(1.0) + }; + let display_w = preview.width as f32 * scale; + let display_h = preview.height as f32 * scale; + + ui.add(egui::Image::new(egui::load::SizedTexture::new( + egui_id, + [display_w, display_h], + ))); + } else { + // Non-texture file info + ui.heading(&file_info.name); + if !preview.format_info.is_empty() { + ui.colored_label(egui::Color32::YELLOW, &preview.format_info); + } + if file_info.size_bytes > 0 { + ui.separator(); + ui.label(format!("Size: {} bytes", file_info.size_bytes)); + if let Some(fmt) = file_info.detected_format { + ui.label(format!("Detected format: {:?}", fmt)); + } + } else if iso_state.loading { + ui.spinner(); + ui.label("Loading file…"); + } + } + }); + } else { + // No file selected — show welcome / RE notes egui::CentralPanel::default().show(ctx, |ui| { ui.heading("Project Sylpheed: Arc of Deception"); ui.label("Asset Viewer — Milestone 1"); ui.separator(); ui.collapsing("Getting Started", |ui| { - ui.label("1. Dump your Project Sylpheed disc to an ISO"); + ui.label("Option A — open an ISO directly:"); + ui.code(" File → Open ISO disc image…"); + ui.separator(); + ui.label("Option B — extract first, then open folder:"); ui.code(" xdvdfs unpack sylpheed.iso ./assets/"); - ui.label("2. Restart the viewer — files will appear in the left panel"); - ui.label("3. Click a .xpr file to preview the texture"); - ui.label("4. Use File → Open to select a different directory"); + ui.code(" File → Open extracted folder…"); + ui.separator(); + ui.label("Click a .xpr file in the left panel to preview the texture."); }); ui.collapsing("Reverse Engineering Notes", |ui| {