diff --git a/Cargo.lock b/Cargo.lock index 488d503..9b38822 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -18,98 +18,6 @@ version = "0.1.10" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "366ffbaa4442f4684d91e2cd7c5ea7c4ed8add41959a31447066e279e432b618" -[[package]] -name = "accesskit" -version = "0.17.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d3d3b8f9bae46a948369bc4a03e815d4ed6d616bd00de4051133a5019dc31c5a" - -[[package]] -name = "accesskit_atspi_common" -version = "0.10.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7c5dd55e6e94949498698daf4d48fb5659e824d7abec0d394089656ceaf99d4f" -dependencies = [ - "accesskit", - "accesskit_consumer", - "atspi-common", - "serde", - "thiserror 1.0.69", - "zvariant 4.2.0", -] - -[[package]] -name = "accesskit_consumer" -version = "0.26.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f47983a1084940ba9a39c077a8c63e55c619388be5476ac04c804cfbd1e63459" -dependencies = [ - "accesskit", - "hashbrown 0.15.5", - "immutable-chunkmap", -] - -[[package]] -name = "accesskit_macos" -version = "0.18.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7329821f3bd1101e03a7d2e03bd339e3ac0dc64c70b4c9f9ae1949e3ba8dece1" -dependencies = [ - "accesskit", - "accesskit_consumer", - "hashbrown 0.15.5", - "objc2 0.5.2", - "objc2-app-kit 0.2.2", - "objc2-foundation 0.2.2", -] - -[[package]] -name = "accesskit_unix" -version = "0.13.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "fcee751cc20d88678c33edaf9c07e8b693cd02819fe89053776f5313492273f5" -dependencies = [ - "accesskit", - "accesskit_atspi_common", - "async-channel", - "async-executor", - "async-task", - "atspi", - "futures-lite", - "futures-util", - "serde", - "zbus 4.4.0", -] - -[[package]] -name = "accesskit_windows" -version = "0.24.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "24fcd5d23d70670992b823e735e859374d694a3d12bfd8dd32bd3bd8bedb5d81" -dependencies = [ - "accesskit", - "accesskit_consumer", - "hashbrown 0.15.5", - "paste", - "static_assertions", - "windows", - "windows-core 0.58.0", -] - -[[package]] -name = "accesskit_winit" -version = "0.23.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6a6a48dad5530b6deb9fc7a52cc6c3bf72cdd9eb8157ac9d32d69f2427a5e879" -dependencies = [ - "accesskit", - "accesskit_macos", - "accesskit_unix", - "accesskit_windows", - "raw-window-handle", - "winit", -] - [[package]] name = "adler2" version = "2.0.1" @@ -149,6 +57,12 @@ dependencies = [ "memchr", ] +[[package]] +name = "allocator-api2" +version = "0.2.21" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "683d7910e743518b0e34f1186f92494becacb047c7b6bf616c96772180fef923" + [[package]] name = "android-activity" version = "0.6.0" @@ -200,6 +114,15 @@ dependencies = [ "num-traits", ] +[[package]] +name = "approx" +version = "0.5.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cab112f0a86d568ea0e627cc1d6be74a1e9cd55214684db5561995f6dad897c6" +dependencies = [ + "num-traits", +] + [[package]] name = "arbitrary" version = "1.4.2" @@ -209,26 +132,6 @@ dependencies = [ "derive_arbitrary", ] -[[package]] -name = "arboard" -version = "3.6.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0348a1c054491f4bfe6ab86a7b6ab1e44e45d899005de92f58b3df180b36ddaf" -dependencies = [ - "clipboard-win", - "image", - "log", - "objc2 0.6.4", - "objc2-app-kit 0.3.2", - "objc2-core-foundation", - "objc2-core-graphics", - "objc2-foundation 0.3.2", - "parking_lot", - "percent-encoding", - "windows-sys 0.60.2", - "x11rb", -] - [[package]] name = "arrayref" version = "0.3.9" @@ -249,11 +152,11 @@ checksum = "175571dd1d178ced59193a6fc02dde1b972eb0bc56c892cde9beeceac5bf0f6b" [[package]] name = "ash" -version = "0.38.0+1.3.281" +version = "0.37.3+1.3.251" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0bb44936d800fea8f016d7f2311c6a4f97aebd5dc86f09906139ec848cf3a46f" +checksum = "39e9c3835d686b0a6084ab4234fcd1b07dbf6e4767dce60874b12356a25ecd4a" dependencies = [ - "libloading", + "libloading 0.7.4", ] [[package]] @@ -437,57 +340,6 @@ version = "1.1.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "1505bd5d3d116872e7271a6d4e16d81d0c8570876c8de68093a09ac269d8aac0" -[[package]] -name = "atspi" -version = "0.22.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "be534b16650e35237bb1ed189ba2aab86ce65e88cc84c66f4935ba38575cecbf" -dependencies = [ - "atspi-common", - "atspi-connection", - "atspi-proxies", -] - -[[package]] -name = "atspi-common" -version = "0.6.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1909ed2dc01d0a17505d89311d192518507e8a056a48148e3598fef5e7bb6ba7" -dependencies = [ - "enumflags2", - "serde", - "static_assertions", - "zbus 4.4.0", - "zbus-lockstep", - "zbus-lockstep-macros", - "zbus_names 3.0.0", - "zvariant 4.2.0", -] - -[[package]] -name = "atspi-connection" -version = "0.6.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "430c5960624a4baaa511c9c0fcc2218e3b58f5dbcc47e6190cafee344b873333" -dependencies = [ - "atspi-common", - "atspi-proxies", - "futures-lite", - "zbus 4.4.0", -] - -[[package]] -name = "atspi-proxies" -version = "0.6.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a5e6c5de3e524cf967569722446bcd458d5032348554d9a17d7d72b041ab7496" -dependencies = [ - "atspi-common", - "serde", - "zbus 4.4.0", - "zvariant 4.2.0", -] - [[package]] name = "autocfg" version = "1.5.0" @@ -502,18 +354,24 @@ checksum = "4cbbc9d0964165b47557570cce6c952866c2678457aca742aafc9fb771d30270" [[package]] name = "bit-set" -version = "0.8.0" +version = "0.5.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "08807e080ed7f9d5433fa9b275196cfc35414f66a0c79d864dc51a0d825231a3" +checksum = "0700ddab506f33b20a03b13996eccd309a48e5ff77d0d95926aa0210fb4e95f1" dependencies = [ "bit-vec", ] [[package]] name = "bit-vec" -version = "0.8.0" +version = "0.6.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5e764a1d40d510daf35e07be9eb06e75770908c27d411ee6c92109c9840eaaf7" +checksum = "349f9b6a179ed607305526ca489b34ad0a41aed5f7980fa90eb03160b69598fb" + +[[package]] +name = "bit_field" +version = "0.10.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1e4b40c7323adcfc0a41c4b88143ed58346ff65a288fc144329c5c45e05d70c6" [[package]] name = "bitflags" @@ -526,9 +384,6 @@ name = "bitflags" version = "2.11.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "843867be96c8daad0d758b57df9392b6d8d271134fce549de6ce169ff98a92af" -dependencies = [ - "serde_core", -] [[package]] name = "bitvec" @@ -595,7 +450,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d1da5ab77c1437701eeff7c88d968729e7766172279eab0676857b3d63af7a6f" dependencies = [ "borsh-derive", - "cfg_aliases", + "cfg_aliases 0.2.1", ] [[package]] @@ -628,6 +483,12 @@ version = "3.20.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "5d20789868f4b01b2f2caec9f5c4e0213b41e3e5702a50157d699ae31ced2fcb" +[[package]] +name = "by_address" +version = "1.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "64fa3c856b712db6612c019f14756e64e4bcea13337a6b33b696333a9eaa2d06" + [[package]] name = "bytecheck" version = "0.6.12" @@ -676,12 +537,6 @@ version = "1.5.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "1fd0f2584146f6f2ef48085050886acf353beff7305ebd1ae69500e27c67f64b" -[[package]] -name = "byteorder-lite" -version = "0.1.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8f1fe948ff07f4bd06c30984e69f5b4899c516a3ef74f34df92a2df2ab535495" - [[package]] name = "bytes" version = "1.11.1" @@ -778,21 +633,18 @@ version = "1.0.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9330f8b2ff13f34540b44e946ef35111825727b38d33286ef986142615121801" +[[package]] +name = "cfg_aliases" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fd16c4719339c4530435d38e511904438d07cce7950afa3718a84ac36c10e89e" + [[package]] name = "cfg_aliases" version = "0.2.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "613afe47fcd5fac7ccf1db93babcb082c5994d996f20b8b159f2ad1658eb5724" -[[package]] -name = "cgl" -version = "0.3.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0ced0551234e87afee12411d535648dd89d2e7f34c78b753395567aff3d447ff" -dependencies = [ - "libc", -] - [[package]] name = "chrono" version = "0.4.44" @@ -816,6 +668,36 @@ dependencies = [ "error-code", ] +[[package]] +name = "clipboard_macos" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9b7f4aaa047ba3c3630b080bb9860894732ff23e2aee290a418909aa6d5df38f" +dependencies = [ + "objc2 0.5.2", + "objc2-app-kit 0.2.2", + "objc2-foundation 0.2.2", +] + +[[package]] +name = "clipboard_wayland" +version = "0.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "003f886bc4e2987729d10c1db3424e7f80809f3fc22dbc16c685738887cb37b8" +dependencies = [ + "smithay-clipboard", +] + +[[package]] +name = "clipboard_x11" +version = "0.4.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bd63e33452ffdafd39924c4f05a5dd1e94db646c779c6bd59148a3d95fff5ad4" +dependencies = [ + "thiserror 2.0.18", + "x11rb", +] + [[package]] name = "codepage" version = "0.1.2" @@ -835,6 +717,43 @@ dependencies = [ "unicode-width", ] +[[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" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7e17887fd17353b65b1b2ef1c526c83e26cd72e74f598a8dc1bee13a48f3d9f6" +dependencies = [ + "com_macros", +] + +[[package]] +name = "com_macros" +version = "0.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d375883580a668c7481ea6631fc1a8863e33cc335bf56bfad8d7e6d4b04b13a5" +dependencies = [ + "com_macros_support", + "proc-macro2", + "syn 1.0.109", +] + +[[package]] +name = "com_macros_support" +version = "0.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ad899a1087a9296d5644792d7cb72b8e34c1bec8e7d4fbc002230169a6e8710c" +dependencies = [ + "proc-macro2", + "quote", + "syn 1.0.109", +] + [[package]] name = "combine" version = "4.6.7" @@ -852,12 +771,10 @@ dependencies = [ "calamine", "chrono", "csv", - "dirs", - "eframe", - "egui", + "dirs 5.0.1", "encoding_rs", "genpdf", - "image", + "iced", "regex", "rfd", "rusqlite", @@ -866,6 +783,7 @@ dependencies = [ "serde", "serde_json", "thiserror 2.0.18", + "tokio", "winres", ] @@ -894,16 +812,6 @@ dependencies = [ "libc", ] -[[package]] -name = "core-foundation" -version = "0.10.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b2a6cd9ae233e7f62ba4e9353e81a88df7fc8a5987b8d445b4d90c879bd156f6" -dependencies = [ - "core-foundation-sys", - "libc", -] - [[package]] name = "core-foundation-sys" version = "0.8.7" @@ -917,7 +825,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "c07782be35f9e1140080c6b96f0d44b739e2278479f64e02fdab4e32dfd8b081" dependencies = [ "bitflags 1.3.2", - "core-foundation 0.9.4", + "core-foundation", "core-graphics-types", "foreign-types", "libc", @@ -930,10 +838,33 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "45390e6114f68f718cc7a830514a96f903cccd70d02a8f6d9f643ac4ba45afaf" dependencies = [ "bitflags 1.3.2", - "core-foundation 0.9.4", + "core-foundation", "libc", ] +[[package]] +name = "cosmic-text" +version = "0.12.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "59fd57d82eb4bfe7ffa9b1cec0c05e2fd378155b47f255a67983cb4afe0e80c2" +dependencies = [ + "bitflags 2.11.0", + "fontdb", + "log", + "rangemap", + "rayon", + "rustc-hash 1.1.0", + "rustybuzz", + "self_cell", + "swash", + "sys-locale", + "ttf-parser 0.21.1", + "unicode-bidi", + "unicode-linebreak", + "unicode-script", + "unicode-segmentation", +] + [[package]] name = "cpufeatures" version = "0.2.17" @@ -952,6 +883,25 @@ dependencies = [ "cfg-if", ] +[[package]] +name = "crossbeam-deque" +version = "0.8.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9dd111b7b7f7d55b72c0a6ae361660ee5853c9af73f70c3c2ef6858b950e2e51" +dependencies = [ + "crossbeam-epoch", + "crossbeam-utils", +] + +[[package]] +name = "crossbeam-epoch" +version = "0.9.18" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5b82ac4a3c2ca9c3460964f020e1402edd5753411d7737aa39c3714ad1b5420e" +dependencies = [ + "crossbeam-utils", +] + [[package]] name = "crossbeam-utils" version = "0.8.21" @@ -995,12 +945,51 @@ dependencies = [ "memchr", ] +[[package]] +name = "ctor-lite" +version = "0.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e162d0c2e2068eb736b71e5597eff0b9944e6b973cd9f37b6a288ab9bf20e300" + [[package]] name = "cursor-icon" version = "1.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "f27ae1dd37df86211c42e150270f82743308803d90a6f6e6651cd730d5e1732f" +[[package]] +name = "d3d12" +version = "0.19.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3e3d747f100290a1ca24b752186f61f6637e1deffe3bf6320de6fcb29510a307" +dependencies = [ + "bitflags 2.11.0", + "libloading 0.8.9", + "winapi", +] + +[[package]] +name = "dark-light" +version = "1.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2a76fa97167fa740dcdbfe18e8895601e1bc36525f09b044e00916e717c03a3c" +dependencies = [ + "dconf_rs", + "detect-desktop-environment", + "dirs 4.0.0", + "objc", + "rust-ini", + "web-sys", + "winreg", + "zbus 4.4.0", +] + +[[package]] +name = "dconf_rs" +version = "0.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7046468a81e6a002061c01e6a7c83139daf91b11c30e66795b13217c2d885c8b" + [[package]] name = "derive_arbitrary" version = "1.4.2" @@ -1023,6 +1012,12 @@ dependencies = [ "syn 2.0.117", ] +[[package]] +name = "detect-desktop-environment" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "21d8ad60dd5b13a4ee6bd8fa2d5d88965c597c67bce32b5fc49c94f55cb50810" + [[package]] name = "digest" version = "0.10.7" @@ -1033,13 +1028,33 @@ dependencies = [ "crypto-common", ] +[[package]] +name = "dirs" +version = "4.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ca3aa72a6f96ea37bbc5aa912f6788242832f75369bdfdadcb0e38423f100059" +dependencies = [ + "dirs-sys 0.3.7", +] + [[package]] name = "dirs" version = "5.0.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "44c45a9d03d6676652bcb5e724c7e988de1acad23a711b5217ab9cbecbec2225" dependencies = [ - "dirs-sys", + "dirs-sys 0.4.1", +] + +[[package]] +name = "dirs-sys" +version = "0.3.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1b1d1d91c932ef41c0f2663aa8b0ca0342d444d842c06914aa0a7e352d0bada6" +dependencies = [ + "libc", + "redox_users", + "winapi", ] [[package]] @@ -1095,17 +1110,14 @@ version = "0.5.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ab8ecd87370524b461f8557c119c405552c396ed91fc0a8eec68679eab26f94a" dependencies = [ - "libloading", + "libloading 0.8.9", ] [[package]] -name = "document-features" -version = "0.2.12" +name = "dlv-list" +version = "0.3.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d4b8a88685455ed29a21542a33abd9cb6510b6b129abadabdcef0f4c55bc8f61" -dependencies = [ - "litrs", -] +checksum = "0688c2a7f92e427f44895cd63841bff7b29f8d7a1648b9e7e07a4a365b2e1257" [[package]] name = "downcast-rs" @@ -1119,6 +1131,47 @@ version = "0.1.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d8b14ccef22fc6f5a8f4d7d768562a182c04ce9a3b3157b91390b52ddfdf1a76" +[[package]] +name = "drm" +version = "0.14.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c5b71449a23fe79542d6527ca572844b2016abf9573c49e43144d546b1735aec" +dependencies = [ + "bitflags 2.11.0", + "bytemuck", + "bytemuck_derive", + "drm-ffi", + "drm-fourcc", + "libc", + "rustix 1.1.4", +] + +[[package]] +name = "drm-ffi" +version = "0.9.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "51a91c9b32ac4e8105dec255e849e0d66e27d7c34d184364fb93e469db08f690" +dependencies = [ + "drm-sys", + "rustix 1.1.4", +] + +[[package]] +name = "drm-fourcc" +version = "2.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0aafbcdb8afc29c1a7ee5fbe53b5d62f4565b35a042a662ca9fecd0b54dae6f4" + +[[package]] +name = "drm-sys" +version = "0.8.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ecc8e1361066d91f5ffccff060a3c3be9c3ecde15be2959c1937595f7a82a9f8" +dependencies = [ + "libc", + "linux-raw-sys 0.9.4", +] + [[package]] name = "dtoa" version = "0.4.8" @@ -1126,133 +1179,10 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "56899898ce76aaf4a0f24d914c97ea6ed976d42fec6ad33fcbb0a1103e07b2b0" [[package]] -name = "ecolor" -version = "0.31.1" +name = "either" +version = "1.15.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "bc4feb366740ded31a004a0e4452fbf84e80ef432ecf8314c485210229672fd1" -dependencies = [ - "bytemuck", - "emath", -] - -[[package]] -name = "eframe" -version = "0.31.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d0dfe0859f3fb1bc6424c57d41e10e9093fe938f426b691e42272c2f336d915c" -dependencies = [ - "ahash 0.8.12", - "bytemuck", - "document-features", - "egui", - "egui-wgpu", - "egui-winit", - "egui_glow", - "glow", - "glutin", - "glutin-winit", - "image", - "js-sys", - "log", - "objc2 0.5.2", - "objc2-app-kit 0.2.2", - "objc2-foundation 0.2.2", - "parking_lot", - "percent-encoding", - "profiling", - "raw-window-handle", - "static_assertions", - "wasm-bindgen", - "wasm-bindgen-futures", - "web-sys", - "web-time", - "winapi", - "windows-sys 0.59.0", - "winit", -] - -[[package]] -name = "egui" -version = "0.31.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "25dd34cec49ab55d85ebf70139cb1ccd29c977ef6b6ba4fe85489d6877ee9ef3" -dependencies = [ - "accesskit", - "ahash 0.8.12", - "bitflags 2.11.0", - "emath", - "epaint", - "log", - "nohash-hasher", - "profiling", -] - -[[package]] -name = "egui-wgpu" -version = "0.31.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d319dfef570f699b6e9114e235e862a2ddcf75f0d1a061de9e1328d92146d820" -dependencies = [ - "ahash 0.8.12", - "bytemuck", - "document-features", - "egui", - "epaint", - "log", - "profiling", - "thiserror 1.0.69", - "type-map", - "web-time", - "wgpu", - "winit", -] - -[[package]] -name = "egui-winit" -version = "0.31.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7d9dfbb78fe4eb9c3a39ad528b90ee5915c252e77bbab9d4ebc576541ab67e13" -dependencies = [ - "accesskit_winit", - "ahash 0.8.12", - "arboard", - "bytemuck", - "egui", - "log", - "profiling", - "raw-window-handle", - "smithay-clipboard", - "web-time", - "webbrowser", - "winit", -] - -[[package]] -name = "egui_glow" -version = "0.31.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "910906e3f042ea6d2378ec12a6fd07698e14ddae68aed2d819ffe944a73aab9e" -dependencies = [ - "ahash 0.8.12", - "bytemuck", - "egui", - "glow", - "log", - "memoffset", - "profiling", - "wasm-bindgen", - "web-sys", - "winit", -] - -[[package]] -name = "emath" -version = "0.31.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9e4cadcff7a5353ba72b7fea76bf2122b5ebdbc68e8155aa56dfdea90083fe1b" -dependencies = [ - "bytemuck", -] +checksum = "48c757948c5ede0e46177b7add2e67155f70e33c07fea8284df6576da70b3719" [[package]] name = "encoding" @@ -1354,30 +1284,6 @@ dependencies = [ "syn 2.0.117", ] -[[package]] -name = "epaint" -version = "0.31.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "41fcc0f5a7c613afd2dee5e4b30c3e6acafb8ad6f0edb06068811f708a67c562" -dependencies = [ - "ab_glyph", - "ahash 0.8.12", - "bytemuck", - "ecolor", - "emath", - "epaint_default_fonts", - "log", - "nohash-hasher", - "parking_lot", - "profiling", -] - -[[package]] -name = "epaint_default_fonts" -version = "0.31.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "fc7e7a64c02cf7a5b51e745a9e45f60660a286f151c238b9d397b3e923f5082f" - [[package]] name = "equivalent" version = "1.0.2" @@ -1400,6 +1306,25 @@ version = "3.3.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "dea2df4cf52843e0452895c455a1a2cfbb842a1e7329671acf418fdc53ed4c59" +[[package]] +name = "etagere" +version = "0.2.15" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fc89bf99e5dc15954a60f707c1e09d7540e5cd9af85fa75caa0b510bc08c5342" +dependencies = [ + "euclid", + "svg_fmt", +] + +[[package]] +name = "euclid" +version = "0.22.13" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "df61bf483e837f88d5c2291dcf55c67be7e676b3a51acc48db3a7b163b91ed63" +dependencies = [ + "num-traits", +] + [[package]] name = "event-listener" version = "5.4.1" @@ -1421,6 +1346,21 @@ dependencies = [ "pin-project-lite", ] +[[package]] +name = "exr" +version = "1.74.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4300e043a56aa2cb633c01af81ca8f699a321879a7854d3896a0ba89056363be" +dependencies = [ + "bit_field", + "half", + "lebe", + "miniz_oxide", + "rayon-core", + "smallvec", + "zune-inflate", +] + [[package]] name = "fallible-iterator" version = "0.3.0" @@ -1433,32 +1373,18 @@ version = "0.1.9" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "7360491ce676a36bf9bb3c56c1aa791658183a54d2744120f27285738d90465a" +[[package]] +name = "fast-srgb8" +version = "1.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dd2e7510819d6fbf51a5545c8f922716ecfb14df168a3242f7d33e0239efe6a1" + [[package]] name = "fastrand" version = "2.3.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "37909eebbb50d72f9059c3b6d82c0463f2ff062c9e95845c43a6c9c0355411be" -[[package]] -name = "fax" -version = "0.2.6" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f05de7d48f37cd6730705cbca900770cab77a89f413d23e100ad7fad7795a0ab" -dependencies = [ - "fax_derive", -] - -[[package]] -name = "fax_derive" -version = "0.2.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a0aca10fb742cb43f9e7bb8467c91aa9bcb8e3ffbc6a6f7389bb93ffc920577d" -dependencies = [ - "proc-macro2", - "quote", - "syn 2.0.117", -] - [[package]] name = "fdeflate" version = "0.3.7" @@ -1490,6 +1416,38 @@ version = "0.1.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d9c4f5dac5e15c24eb999c26181a6ca40b39fe946cbe4c263c7209467bc83af2" +[[package]] +name = "font-types" +version = "0.7.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b3971f9a5ca983419cdc386941ba3b9e1feba01a0ab888adf78739feb2798492" +dependencies = [ + "bytemuck", +] + +[[package]] +name = "fontconfig-parser" +version = "0.5.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bbc773e24e02d4ddd8395fd30dc147524273a83e54e0f312d986ea30de5f5646" +dependencies = [ + "roxmltree", +] + +[[package]] +name = "fontdb" +version = "0.16.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b0299020c3ef3f60f526a4f64ab4a3d4ce116b1acbf24cdd22da0068e5d81dc3" +dependencies = [ + "fontconfig-parser", + "log", + "memmap2", + "slotmap", + "tinyvec", + "ttf-parser 0.20.0", +] + [[package]] name = "foreign-types" version = "0.5.0" @@ -1532,6 +1490,21 @@ version = "2.0.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e6d5a32815ae3f33302d95fdcb2ce17862f8c65363dcfd29360480ba1001fc9c" +[[package]] +name = "futures" +version = "0.3.32" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8b147ee9d1f6d097cef9ce628cd2ee62288d963e16fb287bd9286455b241382d" +dependencies = [ + "futures-channel", + "futures-core", + "futures-executor", + "futures-io", + "futures-sink", + "futures-task", + "futures-util", +] + [[package]] name = "futures-channel" version = "0.3.32" @@ -1539,6 +1512,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "07bbe89c50d7a535e539b8c17bc0b49bdb77747034daa8087407d655f3f7cc1d" dependencies = [ "futures-core", + "futures-sink", ] [[package]] @@ -1547,6 +1521,17 @@ version = "0.3.32" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "7e3450815272ef58cec6d564423f6e755e25379b217b0bc688e295ba24df6b1d" +[[package]] +name = "futures-executor" +version = "0.3.32" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "baf29c38818342a3b26b5b923639e7b1f4a61fc5e76102d4b1981c6dc7a7579d" +dependencies = [ + "futures-core", + "futures-task", + "futures-util", +] + [[package]] name = "futures-io" version = "0.3.32" @@ -1595,6 +1580,7 @@ version = "0.3.32" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "389ca41296e6190b48053de0321d02a77f32f8a5d2461dd38762c0593805c6d6" dependencies = [ + "futures-channel", "futures-core", "futures-io", "futures-macro", @@ -1673,6 +1659,16 @@ dependencies = [ "wasip3", ] +[[package]] +name = "gif" +version = "0.13.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4ae047235e33e2829703574b54fdec96bfbad892062d97fed2f76022287de61b" +dependencies = [ + "color_quant", + "weezl", +] + [[package]] name = "gl_generator" version = "0.14.0" @@ -1685,10 +1681,16 @@ dependencies = [ ] [[package]] -name = "glow" -version = "0.16.0" +name = "glam" +version = "0.25.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c5e5ea60d70410161c8bf5da3fdfeaa1c72ed2c15f8bbb9d19fe3a4fad085f08" +checksum = "151665d9be52f9bb40fc7966565d39666f2d1e69233571b71b87791c7e0528b3" + +[[package]] +name = "glow" +version = "0.13.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bd348e04c43b32574f2de31c8bb397d96c9fcfa1371bd4ca6d8bdc464ab121b1" dependencies = [ "js-sys", "slotmap", @@ -1697,67 +1699,10 @@ dependencies = [ ] [[package]] -name = "glutin" -version = "0.32.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "12124de845cacfebedff80e877bb37b5b75c34c5a4c89e47e1cdd67fb6041325" -dependencies = [ - "bitflags 2.11.0", - "cfg_aliases", - "cgl", - "dispatch2", - "glutin_egl_sys", - "glutin_glx_sys", - "glutin_wgl_sys", - "libloading", - "objc2 0.6.4", - "objc2-app-kit 0.3.2", - "objc2-core-foundation", - "objc2-foundation 0.3.2", - "once_cell", - "raw-window-handle", - "wayland-sys", - "windows-sys 0.52.0", - "x11-dl", -] - -[[package]] -name = "glutin-winit" +name = "glutin_wgl_sys" version = "0.5.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "85edca7075f8fc728f28cb8fbb111a96c3b89e930574369e3e9c27eb75d3788f" -dependencies = [ - "cfg_aliases", - "glutin", - "raw-window-handle", - "winit", -] - -[[package]] -name = "glutin_egl_sys" -version = "0.7.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4c4680ba6195f424febdc3ba46e7a42a0e58743f2edb115297b86d7f8ecc02d2" -dependencies = [ - "gl_generator", - "windows-sys 0.52.0", -] - -[[package]] -name = "glutin_glx_sys" -version = "0.6.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8a7bb2938045a88b612499fbcba375a77198e01306f52272e692f8c1f3751185" -dependencies = [ - "gl_generator", - "x11-dl", -] - -[[package]] -name = "glutin_wgl_sys" -version = "0.6.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2c4ee00b289aba7a9e5306d57c2d05499b2e5dc427f84ac708bd2c090212cf3e" +checksum = "6c8098adac955faa2d31079b65dc48841251f69efd3ac25477903fc424362ead" dependencies = [ "gl_generator", ] @@ -1782,25 +1727,48 @@ dependencies = [ ] [[package]] -name = "gpu-descriptor" -version = "0.3.2" +name = "gpu-allocator" +version = "0.25.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b89c83349105e3732062a895becfc71a8f921bb71ecbbdd8ff99263e3b53a0ca" +checksum = "6f56f6318968d03c18e1bcf4857ff88c61157e9da8e47c5f29055d60e1228884" +dependencies = [ + "log", + "presser", + "thiserror 1.0.69", + "winapi", + "windows", +] + +[[package]] +name = "gpu-descriptor" +version = "0.2.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cc11df1ace8e7e564511f53af41f3e42ddc95b56fd07b3f4445d2a6048bc682c" dependencies = [ "bitflags 2.11.0", "gpu-descriptor-types", - "hashbrown 0.15.5", + "hashbrown 0.14.5", ] [[package]] name = "gpu-descriptor-types" -version = "0.2.0" +version = "0.1.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "fdf242682df893b86f33a73828fb09ca4b2d3bb6cc95249707fc684d27484b91" +checksum = "6bf0b36e6f090b7e1d8a4b49c0cb81c1f8376f72198c65dd3ad9ff3556b8b78c" dependencies = [ "bitflags 2.11.0", ] +[[package]] +name = "guillotiere" +version = "0.6.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b62d5865c036cb1393e23c50693df631d3f5d7bcca4c04fe4cc0fd592e74a782" +dependencies = [ + "euclid", + "svg_fmt", +] + [[package]] name = "half" version = "2.7.1" @@ -1828,6 +1796,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e5274423e17b7c9fc20b6e7e208532f9b19825d82dfd615708b70edd83df41f1" dependencies = [ "ahash 0.8.12", + "allocator-api2", ] [[package]] @@ -1854,6 +1823,21 @@ dependencies = [ "hashbrown 0.14.5", ] +[[package]] +name = "hassle-rs" +version = "0.11.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "af2a7e73e1f34c48da31fb668a907f250794837e08faa144fd24f0b8b741e890" +dependencies = [ + "bitflags 2.11.0", + "com", + "libc", + "libloading 0.8.9", + "thiserror 1.0.69", + "widestring", + "winapi", +] + [[package]] name = "heck" version = "0.5.0" @@ -1902,6 +1886,188 @@ dependencies = [ "cc", ] +[[package]] +name = "iced" +version = "0.13.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "88acfabc84ec077eaf9ede3457ffa3a104626d79022a9bf7f296093b1d60c73f" +dependencies = [ + "iced_core", + "iced_futures", + "iced_renderer", + "iced_widget", + "iced_winit", + "image", + "thiserror 1.0.69", +] + +[[package]] +name = "iced_core" +version = "0.13.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0013a238275494641bf8f1732a23a808196540dc67b22ff97099c044ae4c8a1c" +dependencies = [ + "bitflags 2.11.0", + "bytes", + "dark-light", + "glam", + "log", + "num-traits", + "once_cell", + "palette", + "rustc-hash 2.1.1", + "smol_str", + "thiserror 1.0.69", + "web-time", +] + +[[package]] +name = "iced_futures" +version = "0.13.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0c04a6745ba2e80f32cf01e034fd00d853aa4f4cd8b91888099cb7aaee0d5d7c" +dependencies = [ + "futures", + "iced_core", + "log", + "rustc-hash 2.1.1", + "tokio", + "wasm-bindgen-futures", + "wasm-timer", +] + +[[package]] +name = "iced_glyphon" +version = "0.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "41c3bb56f1820ca252bc1d0994ece33d233a55657c0c263ea7cb16895adbde82" +dependencies = [ + "cosmic-text", + "etagere", + "lru", + "rustc-hash 2.1.1", + "wgpu", +] + +[[package]] +name = "iced_graphics" +version = "0.13.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ba25a18cfa6d5cc160aca7e1b34f73ccdff21680fa8702168c09739767b6c66f" +dependencies = [ + "bitflags 2.11.0", + "bytemuck", + "cosmic-text", + "half", + "iced_core", + "iced_futures", + "image", + "kamadak-exif", + "log", + "once_cell", + "raw-window-handle", + "rustc-hash 2.1.1", + "thiserror 1.0.69", + "unicode-segmentation", +] + +[[package]] +name = "iced_renderer" +version = "0.13.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "73558208059f9e622df2bf434e044ee2f838ce75201a023cf0ca3e1244f46c2a" +dependencies = [ + "iced_graphics", + "iced_tiny_skia", + "iced_wgpu", + "log", + "thiserror 1.0.69", +] + +[[package]] +name = "iced_runtime" +version = "0.13.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "348b5b2c61c934d88ca3b0ed1ed913291e923d086a66fa288ce9669da9ef62b5" +dependencies = [ + "bytes", + "iced_core", + "iced_futures", + "raw-window-handle", + "thiserror 1.0.69", +] + +[[package]] +name = "iced_tiny_skia" +version = "0.13.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c625d368284fcc43b0b36b176f76eff1abebe7959dd58bd8ce6897d641962a50" +dependencies = [ + "bytemuck", + "cosmic-text", + "iced_graphics", + "kurbo", + "log", + "rustc-hash 2.1.1", + "softbuffer", + "tiny-skia", +] + +[[package]] +name = "iced_wgpu" +version = "0.13.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "15708887133671d2bcc6c1d01d1f176f43a64d6cdc3b2bf893396c3ee498295f" +dependencies = [ + "bitflags 2.11.0", + "bytemuck", + "futures", + "glam", + "guillotiere", + "iced_glyphon", + "iced_graphics", + "log", + "once_cell", + "rustc-hash 2.1.1", + "thiserror 1.0.69", + "wgpu", +] + +[[package]] +name = "iced_widget" +version = "0.13.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "81429e1b950b0e4bca65be4c4278fea6678ea782030a411778f26fa9f8983e1d" +dependencies = [ + "iced_renderer", + "iced_runtime", + "num-traits", + "once_cell", + "rustc-hash 2.1.1", + "thiserror 1.0.69", + "unicode-segmentation", +] + +[[package]] +name = "iced_winit" +version = "0.13.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f44cd4e1c594b6334f409282937bf972ba14d31fedf03c23aa595d982a2fda28" +dependencies = [ + "iced_futures", + "iced_graphics", + "iced_runtime", + "log", + "rustc-hash 2.1.1", + "thiserror 1.0.69", + "tracing", + "wasm-bindgen-futures", + "web-sys", + "winapi", + "window_clipboard", + "winit", +] + [[package]] name = "icu_collections" version = "2.1.1" @@ -2012,27 +2178,22 @@ dependencies = [ [[package]] name = "image" -version = "0.25.9" +version = "0.24.9" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e6506c6c10786659413faa717ceebcb8f70731c0a60cbae39795fdf114519c1a" +checksum = "5690139d2f55868e080017335e4b94cb7414274c74f1669c84fb5feba2c9f69d" dependencies = [ "bytemuck", - "byteorder-lite", - "moxcms", + "byteorder", + "color_quant", + "exr", + "gif", + "jpeg-decoder", "num-traits", "png", + "qoi", "tiff", ] -[[package]] -name = "immutable-chunkmap" -version = "2.1.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9a3e98b1520e49e252237edc238a39869da9f3241f2ec19dc788c1d24694d1e4" -dependencies = [ - "arrayvec", -] - [[package]] name = "indexmap" version = "2.13.0" @@ -2045,6 +2206,15 @@ dependencies = [ "serde_core", ] +[[package]] +name = "instant" +version = "0.1.13" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e0242819d153cba4b4b05a5a8f2a7e9bbf97b6055b2a002b395c96b5ff3c0222" +dependencies = [ + "cfg-if", +] + [[package]] name = "itoa" version = "0.4.8" @@ -2089,6 +2259,15 @@ dependencies = [ "libc", ] +[[package]] +name = "jpeg-decoder" +version = "0.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "00810f1d8b74be64b13dbf3db89ac67740615d6c891f0e7b6179326533011a07" +dependencies = [ + "rayon", +] + [[package]] name = "js-sys" version = "0.3.91" @@ -2099,6 +2278,15 @@ dependencies = [ "wasm-bindgen", ] +[[package]] +name = "kamadak-exif" +version = "0.5.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ef4fc70d0ab7e5b6bafa30216a6b48705ea964cdfc29c050f2412295eba58077" +dependencies = [ + "mutate_once", +] + [[package]] name = "khronos-egl" version = "6.0.0" @@ -2106,7 +2294,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "6aae1df220ece3c0ada96b8153459b67eebe9ae9212258bb0134ae60416fdf76" dependencies = [ "libc", - "libloading", + "libloading 0.8.9", "pkg-config", ] @@ -2116,18 +2304,44 @@ version = "3.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e2db585e1d738fc771bf08a151420d3ed193d9d895a36df7f6f8a9456b911ddc" +[[package]] +name = "kurbo" +version = "0.10.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1618d4ebd923e97d67e7cd363d80aef35fe961005cbbbb3d2dad8bdd1bc63440" +dependencies = [ + "arrayvec", + "smallvec", +] + [[package]] name = "leb128fmt" version = "0.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "09edd9e8b54e49e587e4f6295a7d29c3ea94d469cb40ab8ca70b288248a81db2" +[[package]] +name = "lebe" +version = "0.5.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7a79a3332a6609480d7d0c9eab957bca6b455b91bb84e66d19f5ff66294b85b8" + [[package]] name = "libc" version = "0.2.182" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "6800badb6cb2082ffd7b6a67e6125bb39f18782f793520caee8cb8846be06112" +[[package]] +name = "libloading" +version = "0.7.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b67380fd3b2fbe7527a606e18729d21c6f3951633d0500574c4dc22d2d638b9f" +dependencies = [ + "cfg-if", + "winapi", +] + [[package]] name = "libloading" version = "0.8.9" @@ -2138,6 +2352,12 @@ dependencies = [ "windows-link", ] +[[package]] +name = "libm" +version = "0.2.16" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b6d2cec3eae94f9f509c767b45932f1ada8350c4bdb85af2fcab4a3c14807981" + [[package]] name = "libredox" version = "0.1.14" @@ -2173,6 +2393,12 @@ version = "0.4.15" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d26c52dbd32dccf2d10cac7725f8eae5296885fb5703b261f7d0a0739ec807ab" +[[package]] +name = "linux-raw-sys" +version = "0.9.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cd945864f07fe9f5371a27ad7b52a172b4b499999f1d97574c9fa68373937e12" + [[package]] name = "linux-raw-sys" version = "0.12.1" @@ -2185,12 +2411,6 @@ version = "0.8.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "6373607a59f0be73a39b6fe456b8192fcc3585f602af20751600e974dd455e77" -[[package]] -name = "litrs" -version = "1.0.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "11d3d7f243d5c5a8b9bb5d6dd2b1602c0cb0b9db1621bafc7ed66e35ff9fe092" - [[package]] name = "lock_api" version = "0.4.14" @@ -2224,6 +2444,12 @@ dependencies = [ "time", ] +[[package]] +name = "lru" +version = "0.12.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "234cf4f4a04dc1f57e24b96cc0cd600cf2af460d4161ac5ecdd0af8e1f3b2a38" + [[package]] name = "lzw" version = "0.10.0" @@ -2265,9 +2491,9 @@ dependencies = [ [[package]] name = "metal" -version = "0.31.0" +version = "0.27.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f569fb946490b5743ad69813cb19629130ce9374034abe31614a36402d18f99e" +checksum = "c43f73953f8cbe511f021b58f18c3ce1c3d1ae13fe953293e13345bf83217f25" dependencies = [ "bitflags 2.11.0", "block", @@ -2289,34 +2515,39 @@ dependencies = [ ] [[package]] -name = "moxcms" -version = "0.7.11" +name = "mio" +version = "1.1.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ac9557c559cd6fc9867e122e20d2cbefc9ca29d80d027a8e39310920ed2f0a97" +checksum = "a69bcab0ad47271a0234d9422b131806bf3968021e5dc9328caf2d4cd58557fc" dependencies = [ - "num-traits", - "pxfm", + "libc", + "wasi", + "windows-sys 0.61.2", ] [[package]] -name = "naga" -version = "24.0.0" +name = "mutate_once" +version = "0.1.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e380993072e52eef724eddfcde0ed013b0c023c3f0417336ed041aa9f076994e" +checksum = "13d2233c9842d08cfe13f9eac96e207ca6a2ea10b80259ebe8ad0268be27d2af" + +[[package]] +name = "naga" +version = "0.19.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "50e3524642f53d9af419ab5e8dd29d3ba155708267667c2f3f06c88c9e130843" dependencies = [ - "arrayvec", "bit-set", "bitflags 2.11.0", - "cfg_aliases", "codespan-reporting", "hexf-parse", "indexmap", "log", + "num-traits", "rustc-hash 1.1.0", "spirv", - "strum", "termcolor", - "thiserror 2.0.18", + "thiserror 1.0.69", "unicode-xid", ] @@ -2367,17 +2598,11 @@ checksum = "71e2746dc3a24dd78b3cfcb7be93368c6de9963d30f43a6a73998a9cf4b17b46" dependencies = [ "bitflags 2.11.0", "cfg-if", - "cfg_aliases", + "cfg_aliases 0.2.1", "libc", "memoffset", ] -[[package]] -name = "nohash-hasher" -version = "0.2.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2bf50223579dc7cdcfb3bfcacf7069ff68243f8c363f62ffa99cf000a6b9c451" - [[package]] name = "num-traits" version = "0.2.19" @@ -2416,6 +2641,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "915b1b472bc21c53464d6c8461c9d3af805ba1ef837e1cac254428f4a77177b1" dependencies = [ "malloc_buf", + "objc_exception", ] [[package]] @@ -2456,7 +2682,7 @@ dependencies = [ "objc2-core-data", "objc2-core-image", "objc2-foundation 0.2.2", - "objc2-quartz-core", + "objc2-quartz-core 0.2.2", ] [[package]] @@ -2468,8 +2694,6 @@ dependencies = [ "bitflags 2.11.0", "block2 0.6.2", "objc2 0.6.4", - "objc2-core-foundation", - "objc2-core-graphics", "objc2-foundation 0.3.2", ] @@ -2635,6 +2859,18 @@ dependencies = [ "objc2-metal", ] +[[package]] +name = "objc2-quartz-core" +version = "0.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "96c1358452b371bf9f104e21ec536d37a650eb10f7ee379fff67d2e08d537f1f" +dependencies = [ + "bitflags 2.11.0", + "objc2 0.6.4", + "objc2-core-foundation", + "objc2-foundation 0.3.2", +] + [[package]] name = "objc2-symbols" version = "0.2.2" @@ -2660,7 +2896,7 @@ dependencies = [ "objc2-core-location", "objc2-foundation 0.2.2", "objc2-link-presentation", - "objc2-quartz-core", + "objc2-quartz-core 0.2.2", "objc2-symbols", "objc2-uniform-type-identifiers", "objc2-user-notifications", @@ -2690,6 +2926,15 @@ dependencies = [ "objc2-foundation 0.2.2", ] +[[package]] +name = "objc_exception" +version = "0.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ad970fb455818ad6cba4c122ad012fae53ae8b4795f86378bce65e4f6bab2ca4" +dependencies = [ + "cc", +] + [[package]] name = "once_cell" version = "1.21.3" @@ -2722,12 +2967,13 @@ dependencies = [ ] [[package]] -name = "ordered-float" -version = "4.6.0" +name = "ordered-multimap" +version = "0.4.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7bb71e1b3fa6ca1c61f383464aaf2bb0e2f8e772a1f01d486832464de363b951" +checksum = "ccd746e37177e1711c20dd619a1620f34f5c8b569c53590a72dedd5344d8924a" dependencies = [ - "num-traits", + "dlv-list", + "hashbrown 0.12.3", ] [[package]] @@ -2746,7 +2992,31 @@ version = "0.25.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "36820e9051aca1014ddc75770aab4d68bc1e9e632f0f5627c4086bc216fb583b" dependencies = [ - "ttf-parser", + "ttf-parser 0.25.1", +] + +[[package]] +name = "palette" +version = "0.7.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4cbf71184cc5ecc2e4e1baccdb21026c20e5fc3dcf63028a086131b3ab00b6e6" +dependencies = [ + "approx 0.5.1", + "fast-srgb8", + "palette_derive", + "phf", +] + +[[package]] +name = "palette_derive" +version = "0.7.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f5030daf005bface118c096f510ffb781fc28f9ab6a32ab224d8631be6851d30" +dependencies = [ + "by_address", + "proc-macro2", + "quote", + "syn 2.0.117", ] [[package]] @@ -2755,6 +3025,17 @@ version = "2.2.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "f38d5652c16fde515bb1ecef450ab0f6a219d619a7274976324d5e377f7dceba" +[[package]] +name = "parking_lot" +version = "0.11.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7d17b78036a60663b797adeaee46f5c9dfebb86948d1255007a1d6be0271ff99" +dependencies = [ + "instant", + "lock_api", + "parking_lot_core 0.8.6", +] + [[package]] name = "parking_lot" version = "0.12.5" @@ -2762,7 +3043,21 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "93857453250e3077bd71ff98b6a65ea6621a19bb0f559a85248955ac12c45a1a" dependencies = [ "lock_api", - "parking_lot_core", + "parking_lot_core 0.9.12", +] + +[[package]] +name = "parking_lot_core" +version = "0.8.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "60a2cfe6f0ad2bfc16aefa463b497d5c7a5ecd44a23efa72aa342d90177356dc" +dependencies = [ + "cfg-if", + "instant", + "libc", + "redox_syscall 0.2.16", + "smallvec", + "winapi", ] [[package]] @@ -2790,6 +3085,48 @@ version = "2.3.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9b4f627cb1b25917193a259e49bdad08f671f8d9708acfd5fe0a8c1455d87220" +[[package]] +name = "phf" +version = "0.11.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1fd6780a80ae0c52cc120a26a1a42c1ae51b247a253e4e06113d23d2c2edd078" +dependencies = [ + "phf_macros", + "phf_shared", +] + +[[package]] +name = "phf_generator" +version = "0.11.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3c80231409c20246a13fddb31776fb942c38553c51e871f8cbd687a4cfb5843d" +dependencies = [ + "phf_shared", + "rand 0.8.5", +] + +[[package]] +name = "phf_macros" +version = "0.11.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f84ac04429c13a7ff43785d75ad27569f2951ce0ffd30a3321230db2fc727216" +dependencies = [ + "phf_generator", + "phf_shared", + "proc-macro2", + "quote", + "syn 2.0.117", +] + +[[package]] +name = "phf_shared" +version = "0.11.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "67eabc2ef2a60eb7faa00097bd1ffdb5bd28e62bf39990626a582201b7a754e5" +dependencies = [ + "siphasher", +] + [[package]] name = "pin-project" version = "1.1.11" @@ -2816,6 +3153,12 @@ version = "0.2.17" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "a89322df9ebe1c1578d689c92318e070967d1042b512afbe49518723f4e6d5cd" +[[package]] +name = "pin-utils" +version = "0.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8b870d8c151b6f2fb93e84a13146138f05d02ed11c7e7c54f8826aaaf7c9f184" + [[package]] name = "piper" version = "0.2.5" @@ -2841,11 +3184,11 @@ checksum = "b4596b6d070b27117e987119b4dac604f3c58cfb0b191112e24771b2faeac1a6" [[package]] name = "png" -version = "0.18.1" +version = "0.17.16" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "60769b8b31b2a9f263dae2776c37b1b28ae246943cf719eb6946a1db05128a61" +checksum = "82151a2fc869e011c153adc57cf2789ccb8d9906ce52c0b39a6b5697749d7526" dependencies = [ - "bitflags 2.11.0", + "bitflags 1.3.2", "crc32fast", "fdeflate", "flate2", @@ -2899,6 +3242,12 @@ dependencies = [ "zerocopy", ] +[[package]] +name = "presser" +version = "0.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e8cf8e6a8aa66ce33f63993ffc4ea4271eb5b0530a9002db8455ea6050c77bfa" + [[package]] name = "prettyplease" version = "0.2.37" @@ -2972,25 +3321,12 @@ dependencies = [ ] [[package]] -name = "pxfm" -version = "0.1.28" +name = "qoi" +version = "0.4.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b5a041e753da8b807c9255f28de81879c78c876392ff2469cde94799b2896b9d" - -[[package]] -name = "quick-error" -version = "2.0.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a993555f31e5a609f617c12db6250dedcac1b0a85076912c436e6fc9b2c8e6a3" - -[[package]] -name = "quick-xml" -version = "0.30.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "eff6510e86862b57b210fd8cbe8ed3f0d7d600b9c2863cd4549a2e033c66e956" +checksum = "7f6d64c71eb498fe9eae14ce4ec935c555749aef511cca85b5568910d6e48001" dependencies = [ - "memchr", - "serde", + "bytemuck", ] [[package]] @@ -3092,12 +3428,63 @@ dependencies = [ "getrandom 0.3.4", ] +[[package]] +name = "range-alloc" +version = "0.1.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ca45419789ae5a7899559e9512e58ca889e41f04f1f2445e9f4b290ceccd1d08" + +[[package]] +name = "rangemap" +version = "1.7.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "973443cf09a9c8656b574a866ab68dfa19f0867d0340648c7d2f6a71b8a8ea68" + [[package]] name = "raw-window-handle" version = "0.6.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "20675572f6f24e9e76ef639bc5552774ed45f1c30e2951e1e99c59888861c539" +[[package]] +name = "rayon" +version = "1.11.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "368f01d005bf8fd9b1206fb6fa653e6c4a81ceb1466406b81792d87c5677a58f" +dependencies = [ + "either", + "rayon-core", +] + +[[package]] +name = "rayon-core" +version = "1.13.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "22e18b0f0062d30d4230b2e85ff77fdfe4326feb054b9783a3460d8435c8ab91" +dependencies = [ + "crossbeam-deque", + "crossbeam-utils", +] + +[[package]] +name = "read-fonts" +version = "0.22.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "69aacb76b5c29acfb7f90155d39759a29496aebb49395830e928a9703d2eec2f" +dependencies = [ + "bytemuck", + "font-types", +] + +[[package]] +name = "redox_syscall" +version = "0.2.16" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fb5a58c1855b4b6819d59012155603f0b22ad30cad752600aadfcb695265519a" +dependencies = [ + "bitflags 1.3.2", +] + [[package]] name = "redox_syscall" version = "0.4.1" @@ -3233,6 +3620,12 @@ dependencies = [ "syn 1.0.109", ] +[[package]] +name = "roxmltree" +version = "0.20.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6c20b6793b5c2fa6553b250154b78d6d0db37e72700ae35fad9387a46f487c97" + [[package]] name = "rusqlite" version = "0.32.1" @@ -3247,6 +3640,16 @@ dependencies = [ "smallvec", ] +[[package]] +name = "rust-ini" +version = "0.18.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f6d5f2436026b4f6e79dc829837d467cc7e9a55ee40e750d716713540715a2df" +dependencies = [ + "cfg-if", + "ordered-multimap", +] + [[package]] name = "rust_decimal" version = "1.40.0" @@ -3326,8 +3729,8 @@ version = "0.8.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9f61411055101f7b60ecf1041d87fb74205fb20b0c7a723f07ef39174cf6b4c0" dependencies = [ - "approx", - "ordered-float 1.1.1", + "approx 0.3.2", + "ordered-float", "stb_truetype", ] @@ -3337,6 +3740,23 @@ version = "1.0.22" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b39cdef0fa800fc44525c84ccb54a029961a8215f9619753635a9c0d2538d46d" +[[package]] +name = "rustybuzz" +version = "0.14.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cfb9cf8877777222e4a3bc7eb247e398b56baba500c38c1c46842431adc8b55c" +dependencies = [ + "bitflags 2.11.0", + "bytemuck", + "libm", + "smallvec", + "ttf-parser 0.21.1", + "unicode-bidi-mirroring", + "unicode-ccc", + "unicode-properties", + "unicode-script", +] + [[package]] name = "ryu" version = "1.0.23" @@ -3383,6 +3803,12 @@ version = "4.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "1c107b6f4780854c8b126e228ea8869f4d7b71260f962fefb57b996b8959ba6b" +[[package]] +name = "self_cell" +version = "1.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b12e76d157a900eb52e81bc6e9f3069344290341720e9178cde2407113ac8d89" + [[package]] name = "semver" version = "0.9.0" @@ -3512,6 +3938,22 @@ version = "0.1.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e3a9fe34e3e7a50316060351f37187a3f546bce95496156754b601a5fa71b76e" +[[package]] +name = "siphasher" +version = "1.0.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b2aa850e253778c88a04c3d7323b043aeda9d3e30d5971937c1855769763678e" + +[[package]] +name = "skrifa" +version = "0.22.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8e1c44ad1f6c5bdd4eefed8326711b7dbda9ea45dfd36068c427d332aa382cbe" +dependencies = [ + "bytemuck", + "read-fonts", +] + [[package]] name = "slab" version = "0.4.12" @@ -3605,6 +4047,48 @@ dependencies = [ "serde", ] +[[package]] +name = "socket2" +version = "0.6.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "86f4aa3ad99f2088c990dfa82d367e19cb29268ed67c574d10d0a4bfe71f07e0" +dependencies = [ + "libc", + "windows-sys 0.60.2", +] + +[[package]] +name = "softbuffer" +version = "0.4.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "aac18da81ebbf05109ab275b157c22a653bb3c12cf884450179942f81bcbf6c3" +dependencies = [ + "as-raw-xcb-connection", + "bytemuck", + "drm", + "fastrand", + "js-sys", + "memmap2", + "ndk", + "objc2 0.6.4", + "objc2-core-foundation", + "objc2-core-graphics", + "objc2-foundation 0.3.2", + "objc2-quartz-core 0.3.2", + "raw-window-handle", + "redox_syscall 0.5.18", + "rustix 1.1.4", + "tiny-xlib", + "tracing", + "wasm-bindgen", + "wayland-backend", + "wayland-client", + "wayland-sys", + "web-sys", + "windows-sys 0.61.2", + "x11rb", +] + [[package]] name = "spirv" version = "0.3.0+sdk-1.3.268.0" @@ -3700,25 +4184,20 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "6637bab7722d379c8b41ba849228d680cc12d0a45ba1fa2b48f2a30577a06731" [[package]] -name = "strum" -version = "0.26.3" +name = "svg_fmt" +version = "0.4.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8fec0f0aef304996cf250b31b5a10dee7980c85da9d759361292b8bca5a18f06" -dependencies = [ - "strum_macros", -] +checksum = "0193cc4331cfd2f3d2011ef287590868599a2f33c3e69bc22c1a3d3acf9e02fb" [[package]] -name = "strum_macros" -version = "0.26.4" +name = "swash" +version = "0.1.19" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4c6bee85a5a24955dc440386795aa378cd9cf82acd5f764469152d2270e581be" +checksum = "cbd59f3f359ddd2c95af4758c18270eddd9c730dde98598023cdabff472c2ca2" dependencies = [ - "heck", - "proc-macro2", - "quote", - "rustversion", - "syn 2.0.117", + "skrifa", + "yazi", + "zeno", ] [[package]] @@ -3754,6 +4233,15 @@ dependencies = [ "syn 2.0.117", ] +[[package]] +name = "sys-locale" +version = "0.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8eab9a99a024a169fe8a903cf9d4a3b3601109bcc13bd9e3c6fff259138626c4" +dependencies = [ + "libc", +] + [[package]] name = "tap" version = "1.0.1" @@ -3824,16 +4312,13 @@ dependencies = [ [[package]] name = "tiff" -version = "0.10.3" +version = "0.9.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "af9605de7fee8d9551863fd692cce7637f548dbd9db9180fcc07ccc6d26c336f" +checksum = "ba1310fcea54c6a9a4fd1aad794ecc02c31682f6bfbecdf460bf19533eed1e3e" dependencies = [ - "fax", "flate2", - "half", - "quick-error", + "jpeg-decoder", "weezl", - "zune-jpeg", ] [[package]] @@ -3885,6 +4370,7 @@ dependencies = [ "bytemuck", "cfg-if", "log", + "png", "tiny-skia-path", ] @@ -3899,6 +4385,19 @@ dependencies = [ "strict-num", ] +[[package]] +name = "tiny-xlib" +version = "0.2.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0324504befd01cab6e0c994f34b2ffa257849ee019d3fb3b64fb2c858887d89e" +dependencies = [ + "as-raw-xcb-connection", + "ctor-lite", + "libloading 0.8.9", + "pkg-config", + "tracing", +] + [[package]] name = "tinystr" version = "0.8.2" @@ -3924,6 +4423,34 @@ version = "0.1.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "1f3ccbac311fea05f86f61904b462b55fb3df8837a366dfc601a0161d0532f20" +[[package]] +name = "tokio" +version = "1.50.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "27ad5e34374e03cfffefc301becb44e9dc3c17584f414349ebe29ed26661822d" +dependencies = [ + "bytes", + "libc", + "mio", + "parking_lot 0.12.5", + "pin-project-lite", + "signal-hook-registry", + "socket2", + "tokio-macros", + "windows-sys 0.61.2", +] + +[[package]] +name = "tokio-macros" +version = "2.6.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5c55a2eff8b69ce66c84f85e1da1c233edc36ceb85a2058d11b0d6a3c7e7569c" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.117", +] + [[package]] name = "toml" version = "0.5.11" @@ -3995,21 +4522,24 @@ dependencies = [ "once_cell", ] +[[package]] +name = "ttf-parser" +version = "0.20.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "17f77d76d837a7830fe1d4f12b7b4ba4192c1888001c7164257e4bc6d21d96b4" + +[[package]] +name = "ttf-parser" +version = "0.21.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2c591d83f69777866b9126b24c6dd9a18351f177e49d625920d19f989fd31cf8" + [[package]] name = "ttf-parser" version = "0.25.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d2df906b07856748fa3f6e0ad0cbaa047052d4a7dd609e231c4f72cee8c36f31" -[[package]] -name = "type-map" -version = "0.5.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "cb30dbbd9036155e74adad6812e9898d03ec374946234fbcebd5dfc7b9187b90" -dependencies = [ - "rustc-hash 2.1.1", -] - [[package]] name = "typenum" version = "1.19.0" @@ -4027,12 +4557,48 @@ dependencies = [ "winapi", ] +[[package]] +name = "unicode-bidi" +version = "0.3.18" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5c1cb5db39152898a79168971543b1cb5020dff7fe43c8dc468b0885f5e29df5" + +[[package]] +name = "unicode-bidi-mirroring" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "23cb788ffebc92c5948d0e997106233eeb1d8b9512f93f41651f52b6c5f5af86" + +[[package]] +name = "unicode-ccc" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1df77b101bcc4ea3d78dafc5ad7e4f58ceffe0b2b16bf446aeb50b6cb4157656" + [[package]] name = "unicode-ident" version = "1.0.24" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e6e4313cd5fcd3dad5cafa179702e2b244f760991f45397d14d4ebf38247da75" +[[package]] +name = "unicode-linebreak" +version = "0.1.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3b09c83c3c29d37506a3e260c08c03743a6bb66a9cd432c6934ab501a190571f" + +[[package]] +name = "unicode-properties" +version = "0.1.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7df058c713841ad818f1dc5d3fd88063241cc61f49f5fbea4b951e8cf5a8d71d" + +[[package]] +name = "unicode-script" +version = "0.5.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "383ad40bb927465ec0ce7720e033cb4ca06912855fc35db31b5755d0de75b1ee" + [[package]] name = "unicode-segmentation" version = "1.12.0" @@ -4214,6 +4780,21 @@ dependencies = [ "wasmparser", ] +[[package]] +name = "wasm-timer" +version = "0.2.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "be0ecb0db480561e9a7642b5d3e4187c128914e58aa84330b9493e3eb68c5e7f" +dependencies = [ + "futures", + "js-sys", + "parking_lot 0.11.2", + "pin-utils", + "wasm-bindgen", + "wasm-bindgen-futures", + "web-sys", +] + [[package]] name = "wasmparser" version = "0.244.0" @@ -4381,22 +4962,6 @@ dependencies = [ "wasm-bindgen", ] -[[package]] -name = "webbrowser" -version = "1.1.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3f00bb839c1cf1e3036066614cbdcd035ecf215206691ea646aa3c60a24f68f2" -dependencies = [ - "core-foundation 0.10.1", - "jni", - "log", - "ndk-context", - "objc2 0.6.4", - "objc2-foundation 0.3.2", - "url", - "web-sys", -] - [[package]] name = "weezl" version = "0.1.12" @@ -4405,17 +4970,17 @@ checksum = "a28ac98ddc8b9274cb41bb4d9d4d5c425b6020c50c46f25559911905610b4a88" [[package]] name = "wgpu" -version = "24.0.5" +version = "0.19.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6b0b3436f0729f6cdf2e6e9201f3d39dc95813fad61d826c1ed07918b4539353" +checksum = "cbd7311dbd2abcfebaabf1841a2824ed7c8be443a0f29166e5d3c6a53a762c01" dependencies = [ "arrayvec", - "bitflags 2.11.0", - "cfg_aliases", - "document-features", + "cfg-if", + "cfg_aliases 0.1.1", "js-sys", "log", - "parking_lot", + "naga", + "parking_lot 0.12.5", "profiling", "raw-window-handle", "smallvec", @@ -4430,82 +4995,92 @@ dependencies = [ [[package]] name = "wgpu-core" -version = "24.0.5" +version = "0.19.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7f0aa306497a238d169b9dc70659105b4a096859a34894544ca81719242e1499" +checksum = "28b94525fc99ba9e5c9a9e24764f2bc29bad0911a7446c12f446a8277369bf3a" dependencies = [ "arrayvec", "bit-vec", "bitflags 2.11.0", - "cfg_aliases", - "document-features", + "cfg_aliases 0.1.1", + "codespan-reporting", "indexmap", "log", "naga", "once_cell", - "parking_lot", + "parking_lot 0.12.5", "profiling", "raw-window-handle", "rustc-hash 1.1.0", "smallvec", - "thiserror 2.0.18", + "thiserror 1.0.69", + "web-sys", "wgpu-hal", "wgpu-types", ] [[package]] name = "wgpu-hal" -version = "24.0.4" +version = "0.19.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f112f464674ca69f3533248508ee30cb84c67cf06c25ff6800685f5e0294e259" +checksum = "bfabcfc55fd86611a855816326b2d54c3b2fd7972c27ce414291562650552703" dependencies = [ "android_system_properties", "arrayvec", "ash", + "bit-set", "bitflags 2.11.0", - "bytemuck", - "cfg_aliases", + "block", + "cfg_aliases 0.1.1", "core-graphics-types", + "d3d12", "glow", "glutin_wgl_sys", "gpu-alloc", + "gpu-allocator", "gpu-descriptor", + "hassle-rs", "js-sys", "khronos-egl", "libc", - "libloading", + "libloading 0.8.9", "log", "metal", "naga", "ndk-sys 0.5.0+25.2.9519653", "objc", "once_cell", - "ordered-float 4.6.0", - "parking_lot", + "parking_lot 0.12.5", "profiling", + "range-alloc", "raw-window-handle", "renderdoc-sys", "rustc-hash 1.1.0", "smallvec", - "thiserror 2.0.18", + "thiserror 1.0.69", "wasm-bindgen", "web-sys", "wgpu-types", - "windows", + "winapi", ] [[package]] name = "wgpu-types" -version = "24.0.0" +version = "0.19.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "50ac044c0e76c03a0378e7786ac505d010a873665e2d51383dcff8dd227dc69c" +checksum = "b671ff9fb03f78b46ff176494ee1ebe7d603393f42664be55b64dc8d53969805" dependencies = [ "bitflags 2.11.0", "js-sys", - "log", "web-sys", ] +[[package]] +name = "widestring" +version = "1.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "72069c3113ab32ab29e5584db3c6ec55d416895e60715417b5b883a357c3e471" + [[package]] name = "winapi" version = "0.3.9" @@ -4538,25 +5113,35 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "712e227841d057c1ee1cd2fb22fa7e5a5461ae8e48fa2ca79ec42cfc1931183f" [[package]] -name = "windows" -version = "0.58.0" +name = "window_clipboard" +version = "0.4.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "dd04d41d93c4992d421894c18c8b43496aa748dd4c081bac0dc93eb0489272b6" +checksum = "f6d692d46038c433f9daee7ad8757e002a4248c20b0a3fbc991d99521d3bcb6d" dependencies = [ - "windows-core 0.58.0", + "clipboard-win", + "clipboard_macos", + "clipboard_wayland", + "clipboard_x11", + "raw-window-handle", + "thiserror 1.0.69", +] + +[[package]] +name = "windows" +version = "0.52.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e48a53791691ab099e5e2ad123536d0fff50652600abaf43bbf952894110d0be" +dependencies = [ + "windows-core 0.52.0", "windows-targets 0.52.6", ] [[package]] name = "windows-core" -version = "0.58.0" +version = "0.52.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6ba6d44ec8c2591c134257ce647b7ea6b20335bf6379a27dac5f1641fcf59f99" +checksum = "33ab640c8d7e35bf8ba19b884ba838ceb4fba93a4e8c65a9059d08afcfc683d9" dependencies = [ - "windows-implement 0.58.0", - "windows-interface 0.58.0", - "windows-result 0.2.0", - "windows-strings 0.1.0", "windows-targets 0.52.6", ] @@ -4566,22 +5151,11 @@ version = "0.62.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b8e83a14d34d0623b51dce9581199302a221863196a1dde71a7663a4c2be9deb" dependencies = [ - "windows-implement 0.60.2", - "windows-interface 0.59.3", + "windows-implement", + "windows-interface", "windows-link", - "windows-result 0.4.1", - "windows-strings 0.5.1", -] - -[[package]] -name = "windows-implement" -version = "0.58.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2bbd5b46c938e506ecbce286b6628a02171d56153ba733b6c741fc627ec9579b" -dependencies = [ - "proc-macro2", - "quote", - "syn 2.0.117", + "windows-result", + "windows-strings", ] [[package]] @@ -4595,17 +5169,6 @@ dependencies = [ "syn 2.0.117", ] -[[package]] -name = "windows-interface" -version = "0.58.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "053c4c462dc91d3b1504c6fe5a726dd15e216ba718e84a0e46a88fbe5ded3515" -dependencies = [ - "proc-macro2", - "quote", - "syn 2.0.117", -] - [[package]] name = "windows-interface" version = "0.59.3" @@ -4623,15 +5186,6 @@ version = "0.2.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "f0805222e57f7521d6a62e36fa9163bc891acd422f971defe97d64e70d0a4fe5" -[[package]] -name = "windows-result" -version = "0.2.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1d1043d8214f791817bab27572aaa8af63732e11bf84aa21a45a78d6c317ae0e" -dependencies = [ - "windows-targets 0.52.6", -] - [[package]] name = "windows-result" version = "0.4.1" @@ -4641,16 +5195,6 @@ dependencies = [ "windows-link", ] -[[package]] -name = "windows-strings" -version = "0.1.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4cd9b125c486025df0eabcb585e62173c6c9eddcec5d117d3b6e8c30e2ee4d10" -dependencies = [ - "windows-result 0.2.0", - "windows-targets 0.52.6", -] - [[package]] name = "windows-strings" version = "0.5.1" @@ -4970,9 +5514,9 @@ dependencies = [ "block2 0.5.1", "bytemuck", "calloop 0.13.0", - "cfg_aliases", + "cfg_aliases 0.2.1", "concurrent-queue", - "core-foundation 0.9.4", + "core-foundation", "core-graphics", "cursor-icon", "dpi", @@ -5018,6 +5562,15 @@ dependencies = [ "memchr", ] +[[package]] +name = "winreg" +version = "0.10.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "80d0f4e272c85def139476380b12f9ac60926689dd2e01d4923222f40580869d" +dependencies = [ + "winapi", +] + [[package]] name = "winres" version = "0.1.12" @@ -5150,7 +5703,7 @@ dependencies = [ "as-raw-xcb-connection", "gethostname", "libc", - "libloading", + "libloading 0.8.9", "once_cell", "rustix 1.1.4", "x11rb-protocol", @@ -5203,6 +5756,12 @@ version = "0.8.28" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "3ae8337f8a065cfc972643663ea4279e04e7256de865aa66fe25cec5fb912d3f" +[[package]] +name = "yazi" +version = "0.1.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c94451ac9513335b5e23d7a8a2b61a7102398b8cca5160829d313e84c9d98be1" + [[package]] name = "yoke" version = "0.8.1" @@ -5299,30 +5858,6 @@ dependencies = [ "zvariant 5.10.0", ] -[[package]] -name = "zbus-lockstep" -version = "0.4.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4ca2c5dceb099bddaade154055c926bb8ae507a18756ba1d8963fd7b51d8ed1d" -dependencies = [ - "zbus_xml", - "zvariant 4.2.0", -] - -[[package]] -name = "zbus-lockstep-macros" -version = "0.4.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "709ab20fc57cb22af85be7b360239563209258430bccf38d8b979c5a2ae3ecce" -dependencies = [ - "proc-macro2", - "quote", - "syn 2.0.117", - "zbus-lockstep", - "zbus_xml", - "zvariant 4.2.0", -] - [[package]] name = "zbus_macros" version = "4.4.0" @@ -5374,17 +5909,10 @@ dependencies = [ ] [[package]] -name = "zbus_xml" -version = "4.0.0" +name = "zeno" +version = "0.2.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ab3f374552b954f6abb4bd6ce979e6c9b38fb9d0cd7cc68a7d796e70c9f3a233" -dependencies = [ - "quick-xml 0.30.0", - "serde", - "static_assertions", - "zbus_names 3.0.0", - "zvariant 4.2.0", -] +checksum = "dd15f8e0dbb966fd9245e7498c7e9e5055d9e5c8b676b95bd67091cd11a1e697" [[package]] name = "zerocopy" @@ -5496,18 +6024,12 @@ dependencies = [ ] [[package]] -name = "zune-core" -version = "0.4.12" +name = "zune-inflate" +version = "0.2.54" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3f423a2c17029964870cfaabb1f13dfab7d092a62a29a89264f4d36990ca414a" - -[[package]] -name = "zune-jpeg" -version = "0.4.21" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "29ce2c8a9384ad323cf564b67da86e21d3cfdff87908bc1223ed5c99bc792713" +checksum = "73ab332fe2f6680068f3582b16a24f90ad7096d5d39b974d1c0aff0125116f02" dependencies = [ - "zune-core", + "simd-adler32", ] [[package]] diff --git a/Cargo.toml b/Cargo.toml index 73cf4d1..d98b177 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -4,8 +4,8 @@ version = "0.1.0" edition = "2024" [dependencies] -eframe = "0.31" -egui = "0.31" +iced = { version = "0.13", features = ["tokio", "image"] } +tokio = { version = "1", features = ["full"] } rusqlite = { version = "0.32", features = ["bundled"] } csv = "1.3" calamine = "0.26" @@ -20,7 +20,6 @@ dirs = "5" thiserror = "2" regex = "1" rfd = "0.15" -image = { version = "0.25", default-features = false, features = ["ico"] } [build-dependencies] winres = "0.1" diff --git a/MIGRATION_ICED.md b/MIGRATION_ICED.md index 145295d..8867620 100644 --- a/MIGRATION_ICED.md +++ b/MIGRATION_ICED.md @@ -1,675 +1,504 @@ -# Plano de Migração: egui → iced +# Migração egui → iced: Estado e Referência **Branch:** `change-ui` -**Data:** 04/03/2026 -**Versão iced alvo:** 0.13 +**Versão iced:** 0.13.1 +**Última atualização:** 04/03/2026 +**Status:** ✅ **COMPILANDO** — `cargo build` passa sem erros (5 warnings não-bloqueantes, todos em camadas intocáveis) --- -## 1. Escopo da Migração +## Estado Atual -### O que muda +### O que foi feito (100% completo) -Apenas a camada `src/ui/` é reescrita. Nenhuma outra camada é tocada. +| Arquivo | Status | Descrição | +|---|---|---| +| `Cargo.toml` | ✅ | `eframe`/`egui`/`image` removidos; `iced 0.13` + `tokio` adicionados | +| `src/main.rs` | ✅ | Reescrito com `iced::application(...)` | +| `src/ui/mod.rs` | ✅ | `pub mod app; pub mod components; pub mod message; pub mod screens;` | +| `src/ui/message.rs` | ✅ | Enum `Message` completo + enum `ResultadoPendente` | +| `src/ui/app.rs` | ✅ | Reescrito para iced (Elm architecture) — `App`, `update()`, `view()` | +| `src/ui/screens/mod.rs` | ✅ | Re-exports + `pub fn indice_para_letra(idx: usize) -> String` | +| `src/ui/screens/import.rs` | ✅ | Tela de importação — pick_list de layout usa `LayoutSelecionado` | +| `src/ui/screens/selecionar_aba.rs` | ✅ | Tela de seleção de aba XLSX | +| `src/ui/screens/configuracao_colunas.rs` | ✅ | Tela de configuração — pick_list de layout usa `LayoutSelecionado` | +| `src/ui/screens/resultado.rs` | ✅ | Tela de resultado — botão "Nova Análise" usa `Message::NovaAnalise` | +| `src/ui/screens/layouts.rs` | ✅ | Tela de gerenciamento de layouts | +| `src/ui/components/mod.rs` | ✅ | `pub mod modal; pub mod paginacao; pub mod tabela_preview;` | +| `src/ui/components/modal.rs` | ✅ | Overlay real com `stack!` + `mouse_area` | +| `src/ui/components/tabela_preview.rs` | ✅ | Tabela com scroll horizontal e cabeçalho estilo Excel | +| `src/ui/components/paginacao.rs` | ✅ | Controles reutilizáveis de paginação | -| Camada | Status na migração | -|-------------------|--------------------| -| `src/domain/` | Sem alterações | -| `src/application/`| Sem alterações | -| `src/infrastructure/` | Sem alterações | -| `src/ui/` | Reescrita completa | -| `src/main.rs` | Reescrito (entry point muda) | -| `Cargo.toml` | Substituição de dependências | +### O que NÃO foi alterado (intocado por design) + +- `src/domain/` — entidades, serviços, erros +- `src/application/` — casos de uso +- `src/infrastructure/` — leitores CSV/XLSX, gerador PDF, SQLite --- -## 2. Mudanças no Cargo.toml +## Arquitetura da UI -### Remover - -```toml -eframe = "0.31" -egui = "0.31" -image = { version = "0.25", default-features = false, features = ["ico"] } -``` - -### Adicionar - -```toml -iced = { version = "0.13", features = ["tokio", "image"] } -tokio = { version = "1", features = ["full"] } -``` - -> `rfd` permanece. Continua sendo usado para diálogos de arquivo nativos. -> `image` pode ser removido — iced carrega ícones diretamente de bytes. - ---- - -## 3. Arquitetura da UI em iced - -### Modelo mental (Elm Architecture) +### Padrão Elm (iced) ``` -Estado (Model) → view() → Element (o que é renderizado) - ↓ - Interaction (usuário clica/digita) - ↓ - Message (enum tipado) - ↓ - update(msg) → muta o Estado +App (estado) ──→ view(&self) ──→ Element (widgets renderizados) + ↑ ↓ + │ usuário interage + │ ↓ + └── update(&mut self, msg) ←── Message (enum) + retorna Task ``` -Não existe `&mut self` dentro de closures de renderização. A view é uma função pura que lê o estado e retorna `Element`. Mutações só ocorrem em `update()`. +- `view()` é **pura e somente leitura** — nunca muta estado +- Toda mutação acontece **exclusivamente** em `update()` +- Background tasks retornam via `Task::perform(async { ... }, Message::Variante)` -### Estrutura de arquivos proposta +### Estrutura de arquivos ``` src/ -├── main.rs (entry point iced) -├── ui/ -│ ├── mod.rs -│ ├── app.rs (struct App, impl Application) -│ ├── message.rs (enum Message — todos os eventos) -│ ├── screens/ -│ │ ├── mod.rs -│ │ ├── import.rs (view da tela de importação) -│ │ ├── selecionar_aba.rs (view da seleção de aba XLSX) -│ │ ├── configuracao_colunas.rs (view do mapeamento de colunas) -│ │ ├── resultado.rs (view dos resultados) -│ │ └── layouts.rs (view do gerenciamento de layouts) -│ └── components/ -│ ├── mod.rs -│ ├── modal.rs (componente reutilizável de modal) -│ ├── tabela_preview.rs (componente de pré-visualização) -│ └── paginacao.rs (componente reutilizável de paginação) +├── main.rs +└── ui/ + ├── mod.rs + ├── app.rs (struct App + update + view) + ├── message.rs (enum Message + ResultadoPendente) + ├── screens/ + │ ├── mod.rs (re-exports + indice_para_letra) + │ ├── import.rs + │ ├── selecionar_aba.rs + │ ├── configuracao_colunas.rs + │ ├── resultado.rs + │ └── layouts.rs + └── components/ + ├── mod.rs + ├── modal.rs + ├── tabela_preview.rs + └── paginacao.rs ``` --- -## 4. Mapeamento: egui atual → iced equivalente +## Diferenças Críticas da API — iced 0.13 -### 4.1 Estado global (`app.rs`) +Estas diferenças causaram erros de compilação e **devem ser lembradas** em qualquer adição futura: + +### 1. `Command` foi renomeado para `Task` -**egui atual:** ```rust -pub struct App { - pub estado: EstadoApp, // enum de tela ativa - pub modal: Modal, // modal hand-rolled - pub resultado_pendente: Option>, - // ... 15 campos +// ❌ NÃO EXISTE em iced 0.13 +use iced::Command; +Command::none() + +// ✅ CORRETO +use iced::Task; +Task::none() +Task::perform(future, mapper) +``` + +Todos os retornos de `update()` são `Task`. + +### 2. `.align_items()` foi dividido + +```rust +// ❌ NÃO EXISTE em iced 0.13 +row![...].align_items(Alignment::Center) + +// ✅ CORRETO — Row usa align_y, Column usa align_x +row![...].align_y(Alignment::Center) +column![...].align_x(Alignment::Center) +``` + +### 3. `.center_x()` / `.center_y()` exigem argumento `Length` + +```rust +// ❌ NÃO COMPILA +container(...).center_x().center_y() + +// ✅ CORRETO +container(...).center_x(Length::Fill).center_y(Length::Fill) +``` + +### 4. `iced::clipboard::write` retorna `Task` + +```rust +// Pode ser retornado diretamente de update(): +return iced::clipboard::write(texto); +``` + +### 5. Entry point usa API funcional, não trait `Application` + +```rust +// main.rs +fn main() -> iced::Result { + iced::application("Título", App::update, App::view) + .window(iced::window::Settings { ... }) + .run_with(App::new) } -impl eframe::App for App { - fn update(&mut self, ctx: &Context, _frame: &mut eframe::Frame) { ... } +// App::new retorna (Self, Task) +pub fn new() -> (Self, Task) { ... } +``` + +### 6. Lifetimes em funções que retornam `Element` + +Sempre usar `Element<'_, Message>` (não `Element`) em funções que recebem referências: + +```rust +// ❌ Gera warning mismatched_lifetime_syntaxes +pub fn view(app: &App) -> Element + +// ✅ CORRETO +pub fn view(app: &App) -> Element<'_, Message> +``` + +**Armadilha com Vec local:** criar `Vec` local e passar `&vec` para função que retorna `Element<'a>` causa E0515. Solução: filtrar diretamente de dados que vivem no `&app`. + +### 7. Borrow checker com `Arc>` + +```rust +// ❌ Mantém borrow imutável de &self enquanto tenta &mut self +if let Some(conn_arc) = &self.conn { + let conn = conn_arc.lock().unwrap(); + self.exibir_erro(...); // ERRO: borrow ativo +} + +// ✅ Clonar o Arc primeiro (O(1), não clona a Connection) +let conn_arc = self.conn.as_ref().map(Arc::clone); +if let Some(conn_arc) = conn_arc { + let conn = conn_arc.lock().unwrap(); + drop(conn); // liberar antes de &mut self + self.exibir_erro(...); } ``` -**iced equivalente:** +--- + +## Componentes Implementados + +### `App` (src/ui/app.rs) + +**Estado:** ```rust pub struct App { pub estado: EstadoApp, + pub conn: Option>>, + pub banco_foi_recriado: bool, + pub notas_importadas: Vec, + pub caminho_arquivo: Option, // ← populado para CSV e XLSX + pub nome_arquivo: String, + pub tipo_arquivo_atual: TipoArquivo, + pub layout_csv_atual: LayoutCsv, + pub layout_xlsx_atual: LayoutXlsx, + pub nome_layout_atual: String, + pub abas_xlsx: Vec, + pub layouts_salvos: Vec, pub modal: Option, - // ... mesmos campos de dados -} - -impl iced::Application for App { - type Message = Message; - type Executor = iced::executor::Tokio; // async nativo - type Theme = iced::Theme; - type Flags = (); - - fn new(_flags: ()) -> (Self, Command) { - let app = App::default(); - (app, Command::perform(inicializar_banco(), Message::BancoInicializado)) - } - - fn title(&self) -> String { "Comparador de Notas".into() } - - fn update(&mut self, message: Message) -> Command { ... } - - fn view(&self) -> Element { ... } + pub avisos_importacao: Option, + pub pagina_faltantes: usize, + pub pagina_duplicatas: usize, + pub itens_por_pagina: usize, + pub preview_arquivo: Option>>, + pub resultado_anterior: Option, } ``` -**Diferença principal:** `update()` recebe `Message` em vez de operar diretamente sobre eventos de UI. `view()` é somente leitura (`&self`), sem mutações. - ---- - -### 4.2 Enum `EstadoApp` — sem mudanças necessárias - -O enum pode ser mantido idêntico: +**Máquina de estados (`EstadoApp`):** +``` +Importando + → ArquivoSelecionado (CSV) → ConfigurandoColunas + → ArquivoSelecionado (XLSX) → SelecionandoAba → ConfigurandoColunas +ConfigurandoColunas + → ExecutarImportacao → Analisando +Analisando + → AnaliseCompleta (ok, sem excessivos) → ExibindoResultado + → AnaliseCompleta (excessivos) → ConfirmandoIntervalo (+ modal) +ConfirmandoIntervalo + → ModalConfirmado (ConfirmarExpansaoFaltantes) → Analisando → ExibindoResultado + → ModalCancelado → ConfigurandoColunas +ExibindoResultado + → ReanalisarArquivo → Analisando + → IrParaConfiguracaoColunas → ConfigurandoColunas + → NovaAnalise (modal) → Importando (após confirmação) +GerenciandoLayouts + → IrParaImportacao → Importando +``` +**Modal (`EstadoModal` / `AcaoModal`):** ```rust -pub enum EstadoApp { - Importando, - SelecionandoAba { abas: Vec, caminho: PathBuf }, - ConfigurandoColunas, - ExibindoResultado(ResultadoAnalise), - ConfirmandoIntervalo { pre: ResultadoPreAnalise }, - GerenciandoLayouts, - Analisando, +pub enum EstadoModal { + Informacao { titulo: String, mensagem: String }, // definido, não usado ainda + Aviso { titulo: String, mensagem: String }, + Erro { titulo: String, mensagem: String }, + Confirmacao { titulo: String, mensagem: String, acao: AcaoModal }, + InputTexto { titulo: String, mensagem: String, texto: String, acao: AcaoModal }, +} + +pub enum AcaoModal { + ConfirmarExpansaoFaltantes, + ConfirmarExclusaoLayout(i64), + SobrescreverLayout(Layout), + ConfirmarNovaAnalise, + SalvarLayoutConfig, } ``` -A diferença é que a navegação entre estados ocorre em `update()`, não dentro de render functions. +**Comportamento do `ModalCancelado`:** +- Fecha o modal (`self.modal = None`) +- Se o estado for `ConfirmandoIntervalo`, **também** reseta para `ConfigurandoColunas` ---- - -### 4.3 Sistema de mensagens (`message.rs`) - -Toda interação do usuário vira uma variante do enum `Message`. Este é o arquivo central da UI em iced. +### `Message` (src/ui/message.rs) — enum completo ```rust pub enum Message { - // --- Navegação --- - IrParaImportacao, - IrParaConfiguracaoColunas, - IrParaLayouts, - Voltar, + // Inicialização + BancoInicializado(Result<(Arc>, bool, Vec), String>), + LayoutsRecarregados(Vec), - // --- Arquivo --- - SelecionarArquivo, // abre rfd + // Navegação + IrParaImportacao, IrParaConfiguracaoColunas, IrParaLayouts, Voltar, + + // Arquivo + SelecionarArquivo, ArquivoSelecionado(PathBuf), AbaSelecionada(String), + AbaxlsxCarregadas { caminho: PathBuf, abas: Vec, layout_xlsx: LayoutXlsx, nome_layout: String }, + XlsxErroAoCarregar(String), - // --- Background tasks --- - AnaliseCompleta(ResultadoPendente), // resultado do tokio task + // Background + AnaliseCompleta(ResultadoPendente), - // --- Configuração CSV --- - DelimitadorAlterado(char), - EncodingAlterado(String), - LinhaCabecalhoAlterada(usize), - IndiceNumeroAlterado(usize), - IndiceSerieAlterado(usize), - IndiceValorToggle(bool), - IndiceValorAlterado(usize), - IndiceDataToggle(bool), - IndiceDataAlterada(usize), - IndiceDocTipoToggle(bool), - IndiceDocTipoAlterado(usize), + // Config CSV + DelimitadorAlterado(char), EncodingAlterado(String), LinhaCabecalhoAlterada(usize), + IndiceNumeroAlterado(usize), IndiceSerieAlterado(usize), + IndiceValorToggle(bool), IndiceValorAlterado(usize), + IndiceDataToggle(bool), IndiceDataAlterado(usize), + IndiceDocTipoToggle(bool), IndiceDocTipoAlterado(usize), - // --- Configuração XLSX --- + // Config XLSX AbaXlsxAlterada(String), - PosNumeroAlterada(String), - PosSerieAlterada(String), - PosValorToggle(bool), - PosValorAlterada(String), - PosDataToggle(bool), - PosDataAlterada(String), - PosDocTipoToggle(bool), - PosDocTipoAlterada(String), + PosNumeroAlterada(String), PosSerieAlterada(String), + PosValorToggle(bool), PosValorAlterada(String), + PosDataToggle(bool), PosDataAlterada(String), + PosDocTipoToggle(bool), PosDocTipoAlterada(String), - // --- Análise --- - ExecutarImportacao, - ReanalisarArquivo, + // Análise + ExecutarImportacao, ReanalisarArquivo, ConfirmarExpansaoFaltantes, - CancelarExpansao, + CancelarExpansao, // arm existe em update() mas nenhum widget o dispara diretamente + NovaAnalise, // abre modal de confirmação (AcaoModal::ConfirmarNovaAnalise) - // --- Resultado --- - PaginaFaltantesAlterada(usize), - PaginaDuplicatasAlterada(usize), + // Resultado + PaginaFaltantesAlterada(usize), PaginaDuplicatasAlterada(usize), ItensPorPaginaAlterado(usize), - CopiarFaltantes(ChaveSerie), - CopiarDuplicatas(ChaveSerie), - ExportarPdf, - PdfExportado(Result), + CopiarFaltantes(ChaveSerie), CopiarDuplicatas(ChaveSerie), + ExportarPdf, PdfExportado(Result), - // --- Layouts --- - LayoutSelecionado(i64), - SalvarLayout, + // Layouts + LayoutSelecionado(i64), // aplica layout (chama aplicar_layout) + atualiza nome + SalvarLayout, // abre modal InputTexto NomeLayoutAlterado(String), - ExcluirLayout(i64), - ExclusaoConfirmada(i64), - ExportarLayoutJson(i64), - ImportarLayoutJson, - LayoutJsonImportado(String), // conteúdo do arquivo lido + ExcluirLayout(i64), ExclusaoConfirmada(i64), + ExportarLayoutJson(i64), ImportarLayoutJson, LayoutJsonImportado(String), SobrescreverLayout(Layout), - // --- Modal --- - ModalTextoAlterado(String), - ModalConfirmado, - ModalCancelado, + // Modal + ModalTextoAlterado(String), ModalConfirmado, ModalCancelado, - // --- Banco --- - BancoInicializado(Result<(Connection, bool), String>), - LayoutsRecarregados(Vec), + Noop, } ``` ---- +### `modal.rs` (src/ui/components/modal.rs) -### 4.4 Background tasks — substituição do `mpsc::channel` - -**egui atual (manual):** -```rust -// Spawn thread + channel + polling no update() -let (tx, rx) = mpsc::channel(); -app.resultado_pendente = Some(rx); -std::thread::spawn(move || { - let res = executar_trabalho(); - let _ = tx.send(res); -}); -// No update(): try_recv() + ctx.request_repaint() -``` - -**iced equivalente:** -```rust -// Em update(), retornar um Command que executa async e produz Message -Command::perform( - async move { - let importado = importar_csv(&caminho, &layout).await; - // ... pre_analisar, expandir_analise - ResultadoPendente::Concluido { ... } - }, - Message::AnaliseCompleta -) -``` - -Sem `mpsc`, sem `request_repaint()`, sem polling. O iced gerencia o executor. - -**Funções que precisam de wrapper async:** -- `importar_csv` → já é síncrona, wrappear em `tokio::task::spawn_blocking` -- `importar_xlsx` → idem -- `pre_analisar` → idem -- `expandir_analise` → idem -- `exportar_pdf` → idem - ---- - -### 4.5 Sistema de modal - -**egui atual:** `Window::anchor(CENTER_CENTER)` simulando modal — não é bloqueante de fato. - -**iced equivalente:** Overlay real usando `iced::widget::modal` (disponível via `iced_aw` crate) ou implementação própria com `Stack` + `Container` centralizado: +Overlay real: `stack![conteudo, overlay_escuro]`. O `mouse_area` captura cliques fora da caixa e dispara `ModalCancelado`. ```rust -// Componente Modal reutilizável -pub enum EstadoModal { - Informacao { titulo: String, mensagem: String }, - Aviso { titulo: String, mensagem: String }, - Erro { titulo: String, mensagem: String }, - Confirmacao { titulo: String, mensagem: String, acao: AcaoModal }, - InputTexto { titulo: String, mensagem: String, texto: String, acao: AcaoModal }, -} - -// No view(): -fn view_com_modal<'a>(conteudo: Element<'a, Message>, modal: &EstadoModal) -> Element<'a, Message> { - // Stack: conteúdo abaixo + overlay escuro + janela modal no centro - iced::widget::stack![ - conteudo, - mouse_area( - container(view_modal(modal)) - .width(Length::Fill) - .height(Length::Fill) - .style(|_| container::Style { - background: Some(Color::from_rgba(0.0, 0.0, 0.0, 0.5).into()), - ..Default::default() - }) - .center(Length::Fill) - ).on_press(Message::ModalCancelado) - ].into() -} +pub fn view_com_modal<'a>( + conteudo: Element<'a, Message>, + modal: &'a EstadoModal, +) -> Element<'a, Message> ``` -Este modal é **real**: a camada escura captura cliques, não sendo possível interagir com o conteúdo atrás. +Botões da caixa modal: +- Sempre: **"Fechar"** → `ModalCancelado` +- Se `com_confirmar`: **"Confirmar"** → `ModalConfirmado` ---- +### `tabela_preview.rs` (src/ui/components/tabela_preview.rs) -### 4.6 Drag-and-drop de arquivos +Cabeçalho com letras estilo Excel (A, B, C...) + índice numérico. Scroll horizontal. Trunca células em 30 caracteres. Altura fixa de 160px. -**egui atual:** Não suportado. Usuário precisa usar o botão "Selecionar arquivo". +### `paginacao.rs` (src/ui/components/paginacao.rs) -**iced equivalente:** -```rust -// No view() da tela de importação: -iced::widget::drop_zone( - container(text("Arraste um arquivo CSV ou XLSX aqui")) - .center(Length::Fill) - .style(estilo_drop_zone) -) -.on_drop(|paths| { - paths.into_iter().next() - .map(Message::ArquivoSelecionado) - .unwrap_or(Message::Noop) -}) -``` - -> `iced::widget::drop_zone` está disponível a partir do iced 0.13. - ---- - -### 4.7 Temas (dark/light mode) - -**egui atual:** Sem suporte a temas. Usa o tema padrão da plataforma sem controle programático fácil. - -**iced equivalente:** -```rust -fn theme(&self) -> iced::Theme { - match self.tema { - Tema::Claro => iced::Theme::Light, - Tema::Escuro => iced::Theme::Dark, - Tema::Sistema => iced::Theme::default(), // detecta preferência do SO - } -} -``` - -Adicionar ao estado: -```rust -pub tema: Tema, -``` - -E uma mensagem: -```rust -Message::TemaAlterado(Tema), -``` - ---- - -### 4.8 Breadcrumb de etapas - -**egui atual:** -```rust -egui::TopBottomPanel::top("breadcrumb").show(ctx, |ui| { ... }); -``` - -**iced equivalente:** -```rust -fn view_breadcrumb(passo_ativo: usize) -> Element<'static, Message> { - let passos = ["Arquivo", "Colunas", "Resultado"]; - row(passos.iter().enumerate().map(|(i, label)| { - let n = i + 1; - let texto = format!("{}. {}", n, label); - let estilo = if n == passo_ativo { text::Style::default().color(COR_ATIVO) } - else if n < passo_ativo { text::Style::default() } - else { text::Style::default().color(COR_FRACO) }; - // ... separador " › " entre itens - })) - .into() -} -``` - ---- - -### 4.9 Tabela de pré-visualização - -**egui atual:** `egui::Grid` com `end_row()` manual, `id_salt` obrigatório. - -**iced equivalente:** Componente reutilizável via `iced::widget::scrollable` + `column`/`row`: -```rust -pub fn tabela_preview(linhas: &[Vec]) -> Element { - let num_colunas = linhas.iter().map(|l| l.len()).max().unwrap_or(0); - - let cabecalho = row( - (0..num_colunas).map(|i| { - text(format!("{} ({})", indice_para_letra(i), i)) - .font(Font::MONOSPACE) - .size(12) - .width(Length::Fixed(120.0)) - .into() - }) - ); - - let linhas_view = linhas.iter().map(|linha| { - row((0..num_colunas).map(|col| { - let celula = linha.get(col).map(|s| s.as_str()).unwrap_or(""); - let truncado = truncar(celula, 30); - text(truncado).font(Font::MONOSPACE).size(11).width(Length::Fixed(120.0)).into() - })).into() - }); - - scrollable( - column(std::iter::once(cabecalho.into()).chain(linhas_view)) - ) - .direction(scrollable::Direction::Both { ... }) - .into() -} -``` - -Sem `id_salt`, sem `end_row()`. - ---- - -### 4.10 Paginação - -**egui atual:** Código duplicado em `renderizar_faltantes` e `renderizar_duplicatas`, estado `pagina_faltantes`/`pagina_duplicatas` separado no `App`. - -**iced equivalente:** Componente reutilizável: ```rust pub fn controles_paginacao( pagina_atual: usize, total_paginas: usize, msg_anterior: Message, msg_proximo: Message, -) -> Element { - row![ - button("◀").on_press_maybe((pagina_atual > 0).then_some(msg_anterior)), - text(format!("Página {} / {}", pagina_atual + 1, total_paginas)), - button("▶").on_press_maybe((pagina_atual + 1 < total_paginas).then_some(msg_proximo)), - ] - .spacing(8) - .into() -} +) -> Element<'static, Message> ``` +Botões ◀ / ▶ desabilitados automaticamente na primeira/última página via `on_press_maybe`. + --- -### 4.11 Layout right-to-left (botões alinhados à direita) +## Regras de Seleção de Layout (pick_list) -**egui atual:** `ui.with_layout(egui::Layout::right_to_left(...))` — inverte a ordem visual dos botões. +**Em todas as telas** que exibem um `pick_list` de layouts, o handler **deve** usar `LayoutSelecionado(id)`, não `NomeLayoutAlterado`: -**iced equivalente:** ```rust -row![ - text(layout.nome()), - Space::with_width(Length::Fill), // empurra botões para a direita - button("📂 Carregar").on_press(...), - button("📤 Exportar JSON").on_press(...), - button("🗑 Excluir").on_press(...), -] -.spacing(8) -.align_y(Alignment::Center) -``` - -Ordem no código = ordem visual. Sem inversão. - ---- - -### 4.12 Diálogos de arquivo (`rfd`) - -**egui atual:** `rfd::FileDialog::new().pick_file()` chamado diretamente dentro da render function — bloqueia a thread da UI. - -**iced equivalente:** Chamar via `Command::perform` para não bloquear: -```rust -Command::perform( - async { rfd::AsyncFileDialog::new().pick_file().await.map(|h| h.path().to_path_buf()) }, - |resultado| match resultado { - Some(caminho) => Message::ArquivoSelecionado(caminho), - None => Message::Noop, +pick_list(opcoes_layout, nome_sel, { + let layouts = app.layouts_salvos.clone(); + move |nome: String| { + if let Some(id) = layouts.iter().find(|l| l.nome() == nome).and_then(|l| l.id()) { + Message::LayoutSelecionado(id) + } else { + Message::NomeLayoutAlterado(nome) // fallback (nunca deve ocorrer com layouts do banco) + } } +}) +``` + +Isso vale para: `import.rs`, `configuracao_colunas.rs`. A diferença: +- `NomeLayoutAlterado` — só atualiza `self.nome_layout_atual` (string), **não preenche o formulário** +- `LayoutSelecionado(id)` — chama `aplicar_layout()`, que preenche `layout_csv_atual` ou `layout_xlsx_atual` + +--- + +## Fluxo XLSX — Detalhe Crítico + +### Onde `caminho_arquivo` é populado + +Para **CSV**: em `processar_arquivo_selecionado()`, logo após detectar a extensão. + +Para **XLSX**: o caminho fica dentro do estado `SelecionandoAba { caminho }` e é copiado para `self.caminho_arquivo` **somente quando o usuário seleciona uma aba** (handler `AbaSelecionada`). + +``` +ArquivoSelecionado(xlsx) + → AbaxlsxCarregadas + → aba já definida no layout e existe no arquivo? + Sim → caminho_arquivo setado aqui → disparar_importacao() + Não → estado = SelecionandoAba { caminho } ← caminho_arquivo ainda None aqui + → AbaSelecionada(aba) + → caminho_arquivo = Some(caminho) ← só aqui é setado + → estado permanece SelecionandoAba + → IrParaConfiguracaoColunas + → estado = ConfigurandoColunas + → tem_arquivo = true ✓ +``` + +**Consequência:** nunca verificar `app.caminho_arquivo.is_some()` como condição de habilitação antes do usuário ter selecionado uma aba no fluxo XLSX. + +--- + +## Background Tasks + +Todas as operações pesadas usam `Task::perform`: + +```rust +// Padrão para operações síncronas em background: +Task::perform( + async move { + tokio::task::spawn_blocking(move || { + // código síncrono (rusqlite, calamine, etc.) + }) + .await + .unwrap_or_else(|e| ResultadoPendente::Erro(e.to_string())) + }, + Message::AnaliseCompleta, ) ``` -> `rfd::AsyncFileDialog` é o equivalente async. Não bloqueia. - ---- - -## 5. Tela por Tela — O Que Reescrever - -### 5.1 `import.rs` → `screens/import.rs` - -| Elemento atual | Equivalente iced | Observação | -|---|---|---| -| `rfd::FileDialog::pick_file()` síncrono | `rfd::AsyncFileDialog` via `Command::perform` | Não bloqueia | -| `egui::ComboBox` para layouts | `iced::widget::pick_list` | API mais simples | -| Botão "Selecionar arquivo" | Botão + **drop_zone** | Adiciona drag-and-drop | -| Navegação via `app.estado = ...` | `Message::IrParaConfiguracaoColunas` | Via update() | - -**Novidade:** A zona de drop de arquivos vai nesta tela, substituindo/complementando o botão de seleção. - ---- - -### 5.2 `import.rs::renderizar_selecao_aba` → `screens/selecionar_aba.rs` - -| Elemento atual | Equivalente iced | +| Operação | Message de retorno | |---|---| -| `for aba in &abas { ui.selectable_label(...) }` | `column` de `radio` ou `button` por aba | -| Pré-visualização da aba | Componente `tabela_preview` reutilizável | +| Abrir banco + migrations + listar layouts | `BancoInicializado` | +| Listar layouts após salvar/excluir | `LayoutsRecarregados` | +| Listar abas de arquivo XLSX | `AbaxlsxCarregadas` / `XlsxErroAoCarregar` | +| Importar CSV/XLSX + analisar | `AnaliseCompleta` | +| Expandir análise com faltantes | `AnaliseCompleta` | +| Exportar PDF | `PdfExportado` | +| Exportar layout JSON | `Noop` (salva diretamente) | +| Importar layout JSON | `LayoutJsonImportado` | ---- - -### 5.3 `configuracao_colunas.rs` → `screens/configuracao_colunas.rs` - -| Elemento atual | Equivalente iced | -|---|---| -| `egui::DragValue` para índices numéricos | `text_input` com validação numérica ou `number_input` (iced_aw) | -| `egui::Checkbox` para campos opcionais | `iced::widget::checkbox` | -| `egui::ComboBox` para delimitador/encoding/aba | `iced::widget::pick_list` | -| `ui.add_enabled_ui(...)` para desabilitar botão | `button(...).on_press_maybe(valido.then_some(msg))` | -| `ui.colored_label(RED, erro)` | `text(erro).style(Color::from_rgb(0.8, 0.0, 0.0))` | -| Atualização de preview ao mudar delimitador | `Message::DelimitadorAlterado` → `update()` regenera preview | - ---- - -### 5.4 `resultado.rs` → `screens/resultado.rs` - -| Elemento atual | Equivalente iced | -|---|---| -| `resultado.clone()` a cada frame | `&self.estado` lido em `view(&self)` sem clone | -| Paginação duplicada para faltantes e duplicatas | Componente `controles_paginacao` reutilizável | -| `ui.ctx().copy_text(...)` | `iced::clipboard::write(texto)` via Command | -| `egui::ScrollArea::vertical()` | `iced::widget::scrollable` | -| Exportar PDF via `rfd` síncrono | `rfd::AsyncFileDialog` via `Command::perform` | - -**Melhoria possível aqui:** barra de busca/filtro por número, que a tela de resultado atual não tem. Adicionar `Message::FiltroBusca(String)` e filtrar `faltantes_por_serie` na view. - ---- - -### 5.5 `layouts.rs` → `screens/layouts.rs` - -| Elemento atual | Equivalente iced | -|---|---| -| `ui.with_layout(right_to_left)` para alinhar botões | `row![ Space::Fill, botão1, botão2 ]` | -| `rfd` síncrono para importar/exportar JSON | `rfd::AsyncFileDialog` via `Command::perform` | -| `egui::ScrollArea::vertical()` | `iced::widget::scrollable` | - ---- - -### 5.6 `app.rs` — Tela "Analisando" - -**egui atual:** +**Diálogos de arquivo** usam `rfd::AsyncFileDialog` (não bloqueia): ```rust -ui.centered_and_justified(|ui| { - ui.label(egui::RichText::new("⏳ Analisando... aguarde.").size(22.0).strong()); -}); -``` - -**iced equivalente:** -```rust -container( - column![ - text("⏳ Analisando... aguarde.").size(22), - // Opcional: iced::widget::progress_bar indeterminado - ] - .align_x(Alignment::Center) +Task::perform( + async { + rfd::AsyncFileDialog::new() + .add_filter("Planilhas", &["csv", "xlsx", "xls"]) + .pick_file() + .await + .map(|h| h.path().to_path_buf()) + }, + |r| r.map(Message::ArquivoSelecionado).unwrap_or(Message::Noop), ) -.center(Length::Fill) -.into() ``` --- -## 6. Funcionalidades Novas Habilitadas pela Migração +## Warnings Existentes (não-bloqueantes) -Estas funcionalidades não existem no egui mas ficam acessíveis com iced: - -| Funcionalidade | Como implementar em iced | -|---|---| -| **Drag-and-drop de arquivos** | `iced::widget::drop_zone` + `Message::ArquivoSelecionado` | -| **Dark/Light mode** | `fn theme(&self) -> iced::Theme` + `Message::TemaAlterado` | -| **Busca/filtro nos resultados** | `text_input` + filtrar `faltantes_por_serie` na view | -| **Histórico de análises** | Nova tela + tabela + persistência via SQLite (infra não muda) | -| **Gráficos** | `plotters` com backend iced ou `iced_charts` | -| **Modal real bloqueante** | `Stack` + overlay escuro captura eventos — impede cliques atrás | -| **Diálogos não-bloqueantes** | `rfd::AsyncFileDialog` — não trava a UI durante seleção | - ---- - -## 7. O Que Não Muda - -Tudo fora de `src/ui/` permanece intacto: - -- `domain/entities/`: `Nota`, `ChaveSerie`, `Layout`, `ResultadoAnalise`, `ResultadoPreAnalise` -- `domain/services/`: `detector_sequencia`, `detector_duplicidade`, `parser_monetario` -- `domain/errors.rs`: `ErroLayout`, `ResumoAvisos`, todos os erros tipados -- `application/usecases/`: `importar_arquivo`, `executar_analise`, `exportar_pdf`, `layouts` -- `infrastructure/`: `csv_reader`, `xlsx_reader`, `pdf_generator`, `sqlite/` - ---- - -## 8. Ordem Recomendada de Implementação - -A migração deve ser feita de forma que o projeto compile e funcione a cada etapa, nunca quebrando por mais de uma sessão de trabalho. - -### Fase 1 — Esqueleto (sem funcionalidade) - -1. Substituir `eframe`/`egui` por `iced` no `Cargo.toml` -2. Reescrever `main.rs` com `iced::application()` -3. Criar `ui/message.rs` com o enum `Message` completo -4. Criar `ui/app.rs` com struct `App`, impl `Application` mínimo (view retorna `text("em construção")`) -5. Confirmar que compila - -### Fase 2 — Tela de Importação - -1. Implementar `screens/import.rs` com seleção de arquivo (botão + drop_zone) -2. Implementar `Message::ArquivoSelecionado` em `update()` -3. Dropdown de layouts funcionando -4. Navegação básica entre estados - -### Fase 3 — Configuração de Colunas - -1. Implementar `screens/configuracao_colunas.rs` -2. Formulário CSV completo com validação -3. Formulário XLSX completo -4. Componente `tabela_preview` -5. Background task de importação via `Command::perform` - -### Fase 4 — Resultado - -1. Implementar `screens/resultado.rs` -2. Componente `controles_paginacao` reutilizável -3. Copiar para clipboard -4. Exportar PDF via `rfd::AsyncFileDialog` - -### Fase 5 — Layouts e Modal - -1. Implementar `screens/layouts.rs` -2. Implementar componente `modal.rs` -3. Todas as ações de modal (confirmação, input de texto, erro, aviso) - -### Fase 6 — Funcionalidades Novas - -1. Dark/Light mode -2. Busca/filtro nos resultados -3. Histórico de análises (requer nova tela + schema SQLite v4) - ---- - -## 9. Riscos e Mitigações - -| Risco | Probabilidade | Mitigação | +| Warning | Local | Situação | |---|---|---| -| API do iced quebra em versão minor | Alta (projeto pré-1.0) | Fixar versão exata no Cargo.toml: `iced = "=0.13.x"` | -| `iced_aw` (componentes extras) desatualizado | Média | Implementar `number_input` e `modal` internamente se necessário | -| Performance com 100.000 registros na view | Baixa | iced tem renderização incremental; usar `lazy` widget para listas longas | -| `plotters` + iced requer configuração extra | Média | Avaliar na Fase 6; pode ser substituído por barras simples manuais | -| Quebra de compatibilidade na branch `master` | Zero | Branch `change-ui` é isolada; `master` não é afetada | +| `field data is never read` | `domain/nota.rs` | Domínio intocável — ignorar | +| `fields minimo/maximo never read` | `domain/resultado_analise.rs` | Domínio intocável — ignorar | +| `method sem_inconsistencias never used` | `domain/resultado_analise.rs` | Domínio intocável — ignorar | +| `variant Informacao never constructed` | `ui/app.rs` | Mantido para uso futuro (modal informativo) | +| `variant CancelarExpansao never constructed` | `ui/message.rs` | O arm existe em `update()`, mas nenhum widget o dispara; o cancelamento do intervalo ocorre via `ModalCancelado` | --- -## 10. Referências +## O Que Falta Implementar -- Repositório iced: https://github.com/iced-rs/iced -- Exemplos oficiais: https://github.com/iced-rs/iced/tree/master/examples -- `iced_aw` (componentes extras): https://github.com/iced-rs/iced_aw -- `rfd` async: https://docs.rs/rfd/latest/rfd/struct.AsyncFileDialog.html -- `plotters` com iced: https://github.com/plotters-rs/plotters-iced +### Bugs/lacunas conhecidos + +1. **`selecionar_aba.rs` — botões sem diferenciação visual entre aba selecionada e não selecionada** — ambos os branches do `if selecionada` são idênticos (sem estilo diferente). A aba ativa deveria ter destaque visual. + +2. **`selecionar_aba.rs` — layout não é aplicável nessa tela** — se o usuário chegou aqui sem ter selecionado um layout previamente, não há como selecionar um layout antes de avançar para ConfigurandoColunas. Considerar adicionar um `pick_list` de layouts na tela `SelecionandoAba`. + +3. **Sem tema dark/light** — usa o tema padrão do sistema. Para adicionar: + ```rust + // Em main.rs: encadear .theme(|app, _| app.tema.clone()) + // Em App: campo pub tema: iced::Theme + // Mensagem: Message::TemaAlterado(iced::Theme) + ``` + +4. **Sem drag-and-drop de arquivos** — importação tem apenas o botão de seleção. Verificar suporte nativo em iced 0.13 antes de implementar. + +5. **Sem filtro/busca nos resultados** — sem `text_input` para filtrar `faltantes_por_serie`. + +6. **`EstadoModal::Informacao` nunca é usado** — definido mas sem chamadas. Remover ou usar. + +### Funcionalidades futuras (Fase 6 do plano original) + +- Histórico de análises (nova tela + schema SQLite v4) +- Gráficos de completude por série +- Exportação para Excel/CSV + +--- + +## Como Rodar + +```bash +# Verificar sem compilar binário (rápido) +cargo check + +# Compilar (primeira vez ~1 min por baixar iced + wgpu) +cargo build + +# Executar em modo debug +cargo run + +# Build de release (otimizado, sem console no Windows) +cargo build --release +``` + +--- + +## Referências + +- [iced 0.13 changelog](https://github.com/iced-rs/iced/blob/master/CHANGELOG.md) +- [iced exemplos oficiais](https://github.com/iced-rs/iced/tree/master/examples) +- [rfd AsyncFileDialog](https://docs.rs/rfd/latest/rfd/struct.AsyncFileDialog.html) +- [iced::clipboard::write](https://docs.rs/iced/0.13.1/iced/clipboard/fn.write.html) diff --git a/src/main.rs b/src/main.rs index 96d2ed8..3a9bc66 100644 --- a/src/main.rs +++ b/src/main.rs @@ -7,39 +7,12 @@ mod ui; use ui::app::App; -fn load_icon() -> Option { - let bytes = include_bytes!("../icon.ico"); - let img = image::load_from_memory(bytes).ok()?.into_rgba8(); - let (width, height) = img.dimensions(); - Some(egui::viewport::IconData { - rgba: img.into_raw(), - width, - height, - }) -} - -fn main() -> eframe::Result { - let mut viewport = egui::ViewportBuilder::default() - .with_title("Comparador de Notas") - .with_inner_size([1024.0, 768.0]) - .with_min_inner_size([800.0, 600.0]); - - if let Some(icon) = load_icon() { - viewport = viewport.with_icon(std::sync::Arc::new(icon)); - } - - let native_options = eframe::NativeOptions { - viewport, - ..Default::default() - }; - - eframe::run_native( - "Comparador de Notas", - native_options, - Box::new(|_cc| { - let mut app = App::default(); - app.inicializar(); - Ok(Box::new(app)) - }), - ) +fn main() -> iced::Result { + iced::application("Comparador de Notas", App::update, App::view) + .window(iced::window::Settings { + size: iced::Size::new(1024.0, 768.0), + min_size: Some(iced::Size::new(800.0, 600.0)), + ..Default::default() + }) + .run_with(App::new) } diff --git a/src/ui/app.rs b/src/ui/app.rs index 8c87662..b112794 100644 --- a/src/ui/app.rs +++ b/src/ui/app.rs @@ -1,103 +1,61 @@ use crate::application::usecases::{ executar_analise::{expandir_analise, pre_analisar, series_com_intervalo_excessivo}, - importar_arquivo::{importar_csv, importar_xlsx}, - layouts::{excluir_layout, listar_layouts, salvar_layout}, + importar_arquivo::{importar_csv, importar_xlsx, listar_abas_xlsx}, + layouts::{excluir_layout, exportar_layout_json, importar_layout_json, listar_layouts, salvar_layout}, }; use crate::domain::{ entities::{ - chave_serie::ChaveSerie, - layout::{Layout, LayoutCsv, LayoutXlsx, TipoArquivo}, + layout::{Layout, LayoutCsv, LayoutJson, LayoutXlsx, TipoArquivo}, nota::Nota, resultado_analise::{ResultadoAnalise, ResultadoPreAnalise}, }, errors::{ErroLayout, ResumoAvisos}, }; use crate::infrastructure::sqlite::{connection::abrir_banco, migrations::aplicar_migrations}; -use egui::Context; +use crate::ui::message::{Message, ResultadoPendente}; +use iced::widget::{column, container, text, Row}; +use iced::{Alignment, Element, Length, Task}; use rusqlite::Connection; use std::path::PathBuf; -use std::sync::mpsc; +use std::sync::{Arc, Mutex}; + +// ─── Estado global da aplicação ───────────────────────────────────────────── -/// Estado global da aplicação. pub enum EstadoApp { - /// Tela inicial: importar arquivo. Importando, - /// Aguardando seleção de aba de arquivo XLSX. SelecionandoAba { abas: Vec, caminho: PathBuf }, - /// Configuração de colunas após importar arquivo. ConfigurandoColunas, - /// Resultado pronto para exibição. ExibindoResultado(ResultadoAnalise), - /// Aguardando confirmação do usuário para expandir faltantes. ConfirmandoIntervalo { pre: ResultadoPreAnalise }, - /// Gerenciamento de layouts. GerenciandoLayouts, - /// Análise em execução em background (thread separada). Analisando, } -/// Modal bloqueante a ser exibido sobre qualquer tela. -#[derive(Default)] -pub struct Modal { - pub visivel: bool, - pub titulo: String, - pub mensagem: String, - pub tipo: TipoModal, - /// Para modal de confirmação, a ação ao confirmar. - pub acao_confirmacao: Option, - /// Para modal com campo de texto (InputTexto). - pub input_texto: String, +// ─── Modal ─────────────────────────────────────────────────────────────────── + +#[derive(Debug, Clone)] +pub enum EstadoModal { + Informacao { titulo: String, mensagem: String }, + Aviso { titulo: String, mensagem: String }, + Erro { titulo: String, mensagem: String }, + Confirmacao { titulo: String, mensagem: String, acao: AcaoModal }, + InputTexto { titulo: String, mensagem: String, texto: String, acao: AcaoModal }, } -#[derive(Default, Clone, PartialEq)] -pub enum TipoModal { - #[default] - Informacao, - Aviso, - Erro, - Confirmacao, - /// Modal com campo de texto para entrada do usuário. - InputTexto, -} - -#[derive(Clone)] +#[derive(Debug, Clone)] pub enum AcaoModal { ConfirmarExpansaoFaltantes, ConfirmarExclusaoLayout(i64), - /// Sobrescrever layout existente — layout já carrega o id correto. SobrescreverLayout(Layout), ConfirmarNovaAnalise, - /// Salvar a configuração atual como novo layout, usando modal.input_texto como nome. SalvarLayoutConfig, } -/// Resultado enviado pela thread de análise de volta para a UI. -pub enum ResultadoPendente { - /// Análise concluída com sucesso. - Concluido { - resultado: ResultadoAnalise, - /// Avisos de importação (None quando vier do path de expansão). - avisos: Option, - /// Notas importadas (None quando o App já as possui). - notas: Option>, - }, - /// Pré-análise concluída mas precisa de confirmação do usuário. - AguardandoConfirmacao { - pre: ResultadoPreAnalise, - series_excessivas: Vec<(ChaveSerie, u64)>, - avisos: ResumoAvisos, - notas: Vec, - }, - /// Arquivo importado não continha notas válidas. - Vazio, - /// Erro durante importação ou análise. - Erro(String), -} +// ─── App principal ─────────────────────────────────────────────────────────── -/// Struct principal da aplicação egui. pub struct App { pub estado: EstadoApp, - pub conn: Option, + pub conn: Option>>, pub banco_foi_recriado: bool, // Dados em memória do arquivo atual @@ -111,28 +69,27 @@ pub struct App { pub layout_xlsx_atual: LayoutXlsx, pub nome_layout_atual: String, - // Abas XLSX disponíveis (após seleção do arquivo) + // Abas XLSX disponíveis pub abas_xlsx: Vec, // Layouts salvos no banco pub layouts_salvos: Vec, // Modal - pub modal: Modal, + pub modal: Option, // Avisos da última importação pub avisos_importacao: Option, - // Paginação (para tela de resultado) + // Paginação pub pagina_faltantes: usize, pub pagina_duplicatas: usize, pub itens_por_pagina: usize, // Pré-visualização das primeiras linhas do arquivo pub preview_arquivo: Option>>, - // Canal para receber resultado da análise em background - pub resultado_pendente: Option>, - // Resultado anterior, preservado durante reanálise para restaurar em caso de falha + + // Resultado anterior (preservado durante reanálise) pub resultado_anterior: Option, } @@ -151,20 +108,125 @@ impl Default for App { nome_layout_atual: String::new(), abas_xlsx: Vec::new(), layouts_salvos: Vec::new(), - modal: Modal::default(), + modal: None, avisos_importacao: None, pagina_faltantes: 0, pagina_duplicatas: 0, itens_por_pagina: 100, preview_arquivo: None, - resultado_pendente: None, resultado_anterior: None, } } } impl App { - /// Retorna um id numérico da tela atual para evitar borrow conflicts. + // ─── Construtor / inicialização ────────────────────────────────────────── + + pub fn new() -> (Self, Task) { + let app = App::default(); + let cmd = Task::perform( + async { + tokio::task::spawn_blocking(|| { + let (conn, recriado) = abrir_banco().map_err(|e| e.to_string())?; + aplicar_migrations(&conn).map_err(|e| e.to_string())?; + let layouts = listar_layouts(&conn).map_err(|e| e.to_string())?; + Ok::<_, String>((conn, recriado, layouts)) + }) + .await + .map_err(|e| e.to_string()) + .and_then(|r| r) + }, + |result| match result { + Ok((conn, recriado, layouts)) => { + Message::BancoInicializado(Ok(( + Arc::new(Mutex::new(conn)), + recriado, + layouts, + ))) + } + Err(e) => Message::BancoInicializado(Err(e)), + }, + ); + (app, cmd) + } + + // ─── Helpers de modal ─────────────────────────────────────────────────── + + pub fn exibir_erro(&mut self, msg: impl Into) { + self.modal = Some(EstadoModal::Erro { + titulo: "Erro".to_string(), + mensagem: msg.into(), + }); + } + + pub fn exibir_aviso(&mut self, titulo: impl Into, msg: impl Into) { + self.modal = Some(EstadoModal::Aviso { + titulo: titulo.into(), + mensagem: msg.into(), + }); + } + + pub fn exibir_confirmacao( + &mut self, + titulo: impl Into, + msg: impl Into, + acao: AcaoModal, + ) { + self.modal = Some(EstadoModal::Confirmacao { + titulo: titulo.into(), + mensagem: msg.into(), + acao, + }); + } + + pub fn exibir_modal_salvar_layout(&mut self) { + self.modal = Some(EstadoModal::InputTexto { + titulo: "Salvar como layout...".to_string(), + mensagem: "Nome do layout:".to_string(), + texto: self.nome_layout_atual.clone(), + acao: AcaoModal::SalvarLayoutConfig, + }); + } + + // ─── Helpers de layouts ───────────────────────────────────────────────── + + pub fn recarregar_layouts(&mut self) -> Task { + if let Some(conn_arc) = &self.conn { + let conn_arc = conn_arc.clone(); + return Task::perform( + async move { + tokio::task::spawn_blocking(move || { + let conn = conn_arc.lock().unwrap(); + listar_layouts(&conn).map_err(|e| e.to_string()) + }) + .await + .map_err(|e| e.to_string()) + .and_then(|r| r) + }, + |result| match result { + Ok(layouts) => Message::LayoutsRecarregados(layouts), + Err(e) => Message::ModalTextoAlterado(format!("Erro ao recarregar layouts: {}", e)), + }, + ); + } + Task::none() + } + + pub fn aplicar_layout(&mut self, layout: &Layout) { + match layout { + Layout::Csv { config, .. } => { + self.layout_csv_atual = config.clone(); + self.tipo_arquivo_atual = TipoArquivo::Csv; + } + Layout::Xlsx { config, .. } => { + self.layout_xlsx_atual = config.clone(); + self.tipo_arquivo_atual = TipoArquivo::Xlsx; + } + } + } + + // ─── Helpers de tela ──────────────────────────────────────────────────── + pub fn tela_atual(&self) -> u8 { match &self.estado { EstadoApp::Importando => 0, @@ -177,276 +239,629 @@ impl App { } } - /// Inicializa o banco de dados e carrega os layouts salvos. - pub fn inicializar(&mut self) { - match abrir_banco() { - Ok((conn, recriado)) => { - self.banco_foi_recriado = recriado; - if let Err(e) = aplicar_migrations(&conn) { - self.exibir_erro(format!("Erro ao inicializar banco: {}", e)); - return; - } - match listar_layouts(&conn) { - Ok(layouts) => self.layouts_salvos = layouts, - Err(e) => self.exibir_erro(format!("Erro ao carregar layouts: {}", e)), - } - self.conn = Some(conn); + // ─── update ───────────────────────────────────────────────────────────── - if recriado { - self.exibir_aviso( - "Banco de dados corrompido", - "O banco de dados estava corrompido e foi recriado. Os layouts anteriores foram arquivados em config.db.bak.", - ); - } - } - Err(e) => { - self.exibir_erro(format!("Erro crítico ao abrir banco de dados: {}", e)); - } - } - } - - pub fn exibir_erro(&mut self, msg: impl Into) { - self.modal = Modal { - visivel: true, - titulo: "Erro".to_string(), - mensagem: msg.into(), - tipo: TipoModal::Erro, - acao_confirmacao: None, - input_texto: String::new(), - }; - } - - pub fn exibir_aviso(&mut self, titulo: impl Into, msg: impl Into) { - self.modal = Modal { - visivel: true, - titulo: titulo.into(), - mensagem: msg.into(), - tipo: TipoModal::Aviso, - acao_confirmacao: None, - input_texto: String::new(), - }; - } - - pub fn exibir_confirmacao( - &mut self, - titulo: impl Into, - msg: impl Into, - acao: AcaoModal, - ) { - self.modal = Modal { - visivel: true, - titulo: titulo.into(), - mensagem: msg.into(), - tipo: TipoModal::Confirmacao, - acao_confirmacao: Some(acao), - input_texto: String::new(), - }; - } - - /// Recarrega a lista de layouts do banco. - /// Exibe modal com campo de texto para salvar o layout atual como novo preset. - pub fn exibir_modal_salvar_layout(&mut self) { - self.modal = Modal { - visivel: true, - titulo: "Salvar como layout...".to_string(), - mensagem: "Nome do layout:".to_string(), - tipo: TipoModal::InputTexto, - acao_confirmacao: Some(AcaoModal::SalvarLayoutConfig), - input_texto: self.nome_layout_atual.clone(), - }; - } - - pub fn recarregar_layouts(&mut self) { - if let Some(conn) = &self.conn { - match listar_layouts(conn) { - Ok(layouts) => self.layouts_salvos = layouts, - Err(e) => self.exibir_erro(format!("Erro ao carregar layouts: {}", e)), - } - } - } - - /// Renderiza o modal bloqueante, se visível. - pub fn renderizar_modal(&mut self, ctx: &Context) { - if !self.modal.visivel { - return; - } - - let titulo = self.modal.titulo.clone(); - let mensagem = self.modal.mensagem.clone(); - let tipo = self.modal.tipo.clone(); - let acao = self.modal.acao_confirmacao.clone(); - - egui::Window::new(&titulo) - .collapsible(false) - .resizable(false) - .min_width(320.0) - .anchor(egui::Align2::CENTER_CENTER, [0.0, 0.0]) - .show(ctx, |ui| { - ui.add_space(8.0); - ui.label(&mensagem); - - if tipo == TipoModal::InputTexto { - ui.add_space(4.0); - let resp = ui.add( - egui::TextEdit::singleline(&mut self.modal.input_texto) - .desired_width(280.0) - .hint_text("Nome do layout"), - ); - resp.request_focus(); - } - - ui.add_space(12.0); - - ui.horizontal(|ui| { - if tipo == TipoModal::Confirmacao || tipo == TipoModal::InputTexto { - if ui.button("✔ Confirmar").clicked() { - self.modal.visivel = false; - if let Some(acao) = acao.clone() { - self.executar_acao_modal(acao); - } - } - if ui.button("✖ Cancelar").clicked() { - self.modal.visivel = false; - self.modal.input_texto.clear(); - } - } else { - if ui.button("OK").clicked() { - self.modal.visivel = false; - } - } - }); - }); - } - - fn executar_acao_modal(&mut self, acao: AcaoModal) { - match acao { - AcaoModal::ConfirmarExpansaoFaltantes => { - if let EstadoApp::ConfirmandoIntervalo { pre, .. } = - std::mem::replace(&mut self.estado, EstadoApp::Analisando) - { - let notas = self.notas_importadas.clone(); - let (tx, rx) = mpsc::channel(); - self.resultado_pendente = Some(rx); - std::thread::spawn(move || { - let resultado = expandir_analise(pre, ¬as); - let _ = tx.send(ResultadoPendente::Concluido { - resultado, - avisos: None, - notas: None, - }); - }); - } - } - AcaoModal::ConfirmarExclusaoLayout(id) => { - if let Some(conn) = &self.conn { - if let Err(e) = excluir_layout(conn, id) { - self.exibir_erro(e); - } else { - self.recarregar_layouts(); - } - } - } - AcaoModal::SobrescreverLayout(layout) => { - if let Some(conn) = &self.conn { - match salvar_layout(conn, &layout) { - Ok(_) => { - self.recarregar_layouts(); - } - Err(e) => { - self.exibir_erro(e.to_string()); - } - } - } - } - AcaoModal::ConfirmarNovaAnalise => { - self.notas_importadas.clear(); - self.preview_arquivo = None; - self.estado = EstadoApp::Importando; - } - AcaoModal::SalvarLayoutConfig => { - let nome = self.modal.input_texto.trim().to_string(); - self.modal.input_texto.clear(); - if nome.is_empty() { - self.exibir_erro("O nome do layout não pode ser vazio."); - return; - } - let layout = match &self.tipo_arquivo_atual { - TipoArquivo::Csv => Layout::Csv { - id: None, - nome: nome.clone(), - config: self.layout_csv_atual.clone(), - }, - TipoArquivo::Xlsx => Layout::Xlsx { - id: None, - nome: nome.clone(), - config: self.layout_xlsx_atual.clone(), - }, - }; - let resultado = if let Some(conn) = &self.conn { - Some(salvar_layout(conn, &layout)) - } else { - None - }; - match resultado { - Some(Ok(_)) => { - self.nome_layout_atual = nome; - self.recarregar_layouts(); - self.exibir_aviso("Layout salvo", "Layout salvo com sucesso."); - } - Some(Err(ErroLayout::NomeConflitante(nome_conflito))) => { - let id_existente = self - .layouts_salvos - .iter() - .find(|l| l.nome() == nome_conflito) - .and_then(|l| l.id()); - if let Some(id) = id_existente { - let layout_com_id = match &self.tipo_arquivo_atual { - TipoArquivo::Csv => Layout::Csv { - id: Some(id), - nome: nome_conflito.clone(), - config: self.layout_csv_atual.clone(), - }, - TipoArquivo::Xlsx => Layout::Xlsx { - id: Some(id), - nome: nome_conflito.clone(), - config: self.layout_xlsx_atual.clone(), - }, - }; - self.exibir_confirmacao( - "Conflito de nome", - format!( - "Já existe um layout com o nome '{}'. Deseja sobrescrever?", - nome_conflito - ), - AcaoModal::SobrescreverLayout(layout_com_id), + pub fn update(&mut self, message: Message) -> Task { + match message { + // ── Inicialização ── + Message::BancoInicializado(result) => { + match result { + Ok((conn_arc, recriado, layouts)) => { + self.conn = Some(conn_arc); + self.banco_foi_recriado = recriado; + self.layouts_salvos = layouts; + if recriado { + self.exibir_aviso( + "Banco de dados corrompido", + "O banco de dados estava corrompido e foi recriado. Os layouts anteriores foram arquivados em config.db.bak.", ); } } - Some(Err(e)) => self.exibir_erro(e.to_string()), - None => {} + Err(e) => { + self.exibir_erro(format!("Erro crítico ao abrir banco de dados: {}", e)); + } } + Task::none() + } + + Message::LayoutsRecarregados(layouts) => { + self.layouts_salvos = layouts; + Task::none() + } + + // ── Navegação ── + Message::IrParaImportacao => { + self.estado = EstadoApp::Importando; + Task::none() + } + Message::IrParaConfiguracaoColunas => { + self.estado = EstadoApp::ConfigurandoColunas; + Task::none() + } + Message::IrParaLayouts => { + self.estado = EstadoApp::GerenciandoLayouts; + Task::none() + } + Message::Voltar => { + self.estado = EstadoApp::Importando; + Task::none() + } + + // ── Arquivo ── + Message::SelecionarArquivo => Task::perform( + async { + rfd::AsyncFileDialog::new() + .add_filter("Planilhas", &["csv", "xlsx", "xls"]) + .pick_file() + .await + .map(|h| h.path().to_path_buf()) + }, + |resultado| match resultado { + Some(caminho) => Message::ArquivoSelecionado(caminho), + None => Message::Noop, + }, + ), + + Message::ArquivoSelecionado(caminho) => { + self.processar_arquivo_selecionado(caminho) + } + + Message::AbaSelecionada(aba) => { + let caminho = match &self.estado { + EstadoApp::SelecionandoAba { caminho, .. } => caminho.clone(), + _ => return Task::none(), + }; + // Persistir o caminho para que tem_arquivo = true na tela de configuração + self.nome_arquivo = caminho + .file_name() + .map(|n| n.to_string_lossy().to_string()) + .unwrap_or_default(); + self.caminho_arquivo = Some(caminho.clone()); + self.layout_xlsx_atual.aba = aba.clone(); + // Atualizar preview + self.preview_arquivo = + crate::infrastructure::xlsx_reader::preview_xlsx(&caminho, &aba).ok(); + Task::none() + } + + // ── Configuração CSV ── + Message::DelimitadorAlterado(delim) => { + self.layout_csv_atual.delimitador = delim; + if let Some(caminho) = &self.caminho_arquivo.clone() { + self.preview_arquivo = crate::infrastructure::csv_reader::preview_csv( + caminho, + delim as u8, + &self.layout_csv_atual.encoding.clone(), + 5, + ) + .ok(); + } + Task::none() + } + Message::EncodingAlterado(enc) => { + self.layout_csv_atual.encoding = enc; + Task::none() + } + Message::LinhaCabecalhoAlterada(n) => { + self.layout_csv_atual.linha_cabecalho = n; + Task::none() + } + Message::IndiceNumeroAlterado(n) => { + self.layout_csv_atual.indice_numero = n; + Task::none() + } + Message::IndiceSerieAlterado(n) => { + self.layout_csv_atual.indice_serie = n; + Task::none() + } + Message::IndiceValorToggle(ativo) => { + self.layout_csv_atual.indice_valor = if ativo { Some(0) } else { None }; + Task::none() + } + Message::IndiceValorAlterado(n) => { + self.layout_csv_atual.indice_valor = Some(n); + Task::none() + } + Message::IndiceDataToggle(ativo) => { + self.layout_csv_atual.indice_data = if ativo { Some(0) } else { None }; + Task::none() + } + Message::IndiceDataAlterado(n) => { + self.layout_csv_atual.indice_data = Some(n); + Task::none() + } + Message::IndiceDocTipoToggle(ativo) => { + self.layout_csv_atual.indice_documento_tipo = if ativo { Some(0) } else { None }; + Task::none() + } + Message::IndiceDocTipoAlterado(n) => { + self.layout_csv_atual.indice_documento_tipo = Some(n); + Task::none() + } + + // ── Configuração XLSX ── + Message::AbaXlsxAlterada(aba) => { + self.layout_xlsx_atual.aba = aba; + Task::none() + } + Message::PosNumeroAlterada(s) => { + self.layout_xlsx_atual.pos_numero = s; + Task::none() + } + Message::PosSerieAlterada(s) => { + self.layout_xlsx_atual.pos_serie = s; + Task::none() + } + Message::PosValorToggle(ativo) => { + self.layout_xlsx_atual.pos_valor = if ativo { Some(String::new()) } else { None }; + Task::none() + } + Message::PosValorAlterada(s) => { + self.layout_xlsx_atual.pos_valor = Some(s); + Task::none() + } + Message::PosDataToggle(ativo) => { + self.layout_xlsx_atual.pos_data = if ativo { Some(String::new()) } else { None }; + Task::none() + } + Message::PosDataAlterada(s) => { + self.layout_xlsx_atual.pos_data = Some(s); + Task::none() + } + Message::PosDocTipoToggle(ativo) => { + self.layout_xlsx_atual.pos_documento_tipo = + if ativo { Some(String::new()) } else { None }; + Task::none() + } + Message::PosDocTipoAlterada(s) => { + self.layout_xlsx_atual.pos_documento_tipo = Some(s); + Task::none() + } + + // ── Análise ── + Message::ExecutarImportacao => self.disparar_importacao(), + + Message::ReanalisarArquivo => self.reimportar_e_analisar(), + + Message::ConfirmarExpansaoFaltantes => { + if let EstadoApp::ConfirmandoIntervalo { pre } = + std::mem::replace(&mut self.estado, EstadoApp::Analisando) + { + let notas = self.notas_importadas.clone(); + return Task::perform( + async move { + tokio::task::spawn_blocking(move || { + let resultado = expandir_analise(pre, ¬as); + ResultadoPendente::Concluido { + resultado, + avisos: None, + notas: None, + } + }) + .await + .unwrap_or_else(|e| ResultadoPendente::Erro(e.to_string())) + }, + Message::AnaliseCompleta, + ); + } + Task::none() + } + + Message::CancelarExpansao => { + self.estado = EstadoApp::ConfigurandoColunas; + self.modal = None; + Task::none() + } + + Message::NovaAnalise => { + self.exibir_confirmacao( + "Nova Análise", + "Deseja iniciar uma nova análise? O arquivo atual será descartado.", + AcaoModal::ConfirmarNovaAnalise, + ); + Task::none() + } + + Message::AnaliseCompleta(resultado) => { + self.processar_resultado_pendente(resultado); + Task::none() + } + + // ── Resultado ── + Message::PaginaFaltantesAlterada(p) => { + self.pagina_faltantes = p; + Task::none() + } + Message::PaginaDuplicatasAlterada(p) => { + self.pagina_duplicatas = p; + Task::none() + } + Message::ItensPorPaginaAlterado(n) => { + self.itens_por_pagina = n; + self.pagina_faltantes = 0; + self.pagina_duplicatas = 0; + Task::none() + } + Message::CopiarFaltantes(chave) => { + if let EstadoApp::ExibindoResultado(ref resultado) = self.estado { + if let Some(faltantes) = resultado.faltantes_por_serie.get(&chave) { + let texto = faltantes + .iter() + .map(|n| n.to_string()) + .collect::>() + .join(", "); + return iced::clipboard::write(texto); + } + } + Task::none() + } + Message::CopiarDuplicatas(chave) => { + if let EstadoApp::ExibindoResultado(ref resultado) = self.estado { + if let Some(duplicatas) = resultado.duplicadas_por_serie.get(&chave) { + let texto = duplicatas + .iter() + .map(|(n, c)| format!("{} ({}x)", n, c)) + .collect::>() + .join(", "); + return iced::clipboard::write(texto); + } + } + Task::none() + } + Message::ExportarPdf => self.exportar_pdf(), + Message::PdfExportado(resultado) => { + match resultado { + Ok(caminho) => self.exibir_aviso( + "Sucesso", + format!("PDF exportado para: {}", caminho.display()), + ), + Err(e) => self.exibir_erro(format!("Erro ao exportar PDF: {}", e)), + } + Task::none() + } + + // ── Layouts ── + Message::LayoutSelecionado(id) => { + if let Some(layout) = self.layouts_salvos.iter().find(|l| l.id() == Some(id)).cloned() { + self.nome_layout_atual = layout.nome().to_string(); + self.aplicar_layout(&layout); + } + Task::none() + } + Message::NomeLayoutAlterado(nome) => { + self.nome_layout_atual = nome; + Task::none() + } + Message::SalvarLayout => { + self.exibir_modal_salvar_layout(); + Task::none() + } + Message::ExcluirLayout(id) => { + let nome = self + .layouts_salvos + .iter() + .find(|l| l.id() == Some(id)) + .map(|l| l.nome().to_string()) + .unwrap_or_default(); + self.exibir_confirmacao( + "Confirmar exclusão", + format!("Deseja excluir o layout '{}'?", nome), + AcaoModal::ConfirmarExclusaoLayout(id), + ); + Task::none() + } + Message::ExclusaoConfirmada(id) => { + let conn_arc = self.conn.as_ref().map(Arc::clone); + if let Some(conn_arc) = conn_arc { + let conn = conn_arc.lock().unwrap(); + if let Err(e) = excluir_layout(&conn, id) { + drop(conn); + self.exibir_erro(e); + } + } + self.recarregar_layouts() + } + Message::ExportarLayoutJson(id) => self.exportar_layout_json(id), + Message::ImportarLayoutJson => Task::perform( + async { + let file = rfd::AsyncFileDialog::new() + .add_filter("JSON", &["json"]) + .pick_file() + .await; + if let Some(handle) = file { + match tokio::fs::read_to_string(handle.path()).await { + Ok(conteudo) => Some(conteudo), + Err(_) => None, + } + } else { + None + } + }, + |resultado| match resultado { + Some(conteudo) => Message::LayoutJsonImportado(conteudo), + None => Message::Noop, + }, + ), + Message::LayoutJsonImportado(conteudo) => { + self.processar_importacao_json(&conteudo) + } + Message::SobrescreverLayout(layout) => { + let conn_arc = self.conn.as_ref().map(Arc::clone); + if let Some(conn_arc) = conn_arc { + let conn = conn_arc.lock().unwrap(); + match salvar_layout(&conn, &layout) { + Ok(_) => {} + Err(e) => { + drop(conn); + self.exibir_erro(e.to_string()); + return Task::none(); + } + } + } + self.recarregar_layouts() + } + + // ── Modal ── + Message::ModalTextoAlterado(texto) => { + if let Some(EstadoModal::InputTexto { texto: t, .. }) = &mut self.modal { + *t = texto; + } + Task::none() + } + Message::ModalConfirmado => { + let modal = self.modal.take(); + if let Some(estado) = modal { + match estado { + EstadoModal::Confirmacao { acao, .. } + | EstadoModal::InputTexto { acao, .. } => { + return self.executar_acao_modal(acao); + } + _ => {} + } + } + Task::none() + } + Message::ModalCancelado => { + self.modal = None; + // If cancelling while waiting for interval confirmation, go back to config + if matches!(self.estado, EstadoApp::ConfirmandoIntervalo { .. }) { + self.estado = EstadoApp::ConfigurandoColunas; + } + Task::none() + } + + // ── XLSX: abas carregadas ── + Message::AbaxlsxCarregadas { caminho, abas, layout_xlsx, nome_layout } => { + self.abas_xlsx = abas.clone(); + self.nome_layout_atual = nome_layout; + self.layout_xlsx_atual = layout_xlsx.clone(); + + // Se o layout tem aba compatível, disparar análise direto + if !layout_xlsx.aba.is_empty() && abas.contains(&layout_xlsx.aba) { + self.nome_arquivo = caminho + .file_name() + .map(|n| n.to_string_lossy().to_string()) + .unwrap_or_default(); + self.caminho_arquivo = Some(caminho.clone()); + self.layout_xlsx_atual.aba = layout_xlsx.aba.clone(); + self.preview_arquivo = + crate::infrastructure::xlsx_reader::preview_xlsx(&caminho, &layout_xlsx.aba).ok(); + self.disparar_importacao() + } else { + self.estado = EstadoApp::SelecionandoAba { abas, caminho }; + Task::none() + } + } + + Message::XlsxErroAoCarregar(e) => { + self.exibir_erro(format!("Erro ao ler abas do arquivo: {}", e)); + Task::none() + } + + Message::Noop => Task::none(), + } + } + + // ─── view ──────────────────────────────────────────────────────────────── + + pub fn view(&self) -> Element<'_, Message> { + let conteudo = match &self.estado { + EstadoApp::Importando => crate::ui::screens::import::view(self), + EstadoApp::SelecionandoAba { abas, caminho } => { + crate::ui::screens::selecionar_aba::view(self, abas, caminho) + } + EstadoApp::ConfigurandoColunas | EstadoApp::ConfirmandoIntervalo { .. } => { + crate::ui::screens::configuracao_colunas::view(self) + } + EstadoApp::ExibindoResultado(resultado) => { + crate::ui::screens::resultado::view(self, resultado) + } + EstadoApp::GerenciandoLayouts => crate::ui::screens::layouts::view(self), + EstadoApp::Analisando => { + container( + column![ + text("Analisando... aguarde.").size(22), + ] + .align_x(Alignment::Center), + ) + .width(Length::Fill) + .height(Length::Fill) + .center_x(Length::Fill) + .center_y(Length::Fill) + .into() + } + }; + + // Breadcrumb no topo (exceto Layouts e Analisando) + let tela = self.tela_atual(); + let com_breadcrumb = if tela != 5 && tela != 6 { + let breadcrumb = self.view_breadcrumb(); + column![breadcrumb, conteudo] + .width(Length::Fill) + .height(Length::Fill) + .into() + } else { + conteudo + }; + + // Modal por cima de tudo + if let Some(modal) = &self.modal { + crate::ui::components::modal::view_com_modal(com_breadcrumb, modal) + } else { + com_breadcrumb + } + } + + // ─── Breadcrumb ────────────────────────────────────────────────────────── + + fn view_breadcrumb(&self) -> Element<'_, Message> { + let passo_ativo: usize = match self.tela_atual() { + 0 | 1 => 1, + 2 | 4 => 2, + 3 => 3, + _ => 1, + }; + + let passos = [("1. Arquivo", 1), ("2. Colunas", 2), ("3. Resultado", 3)]; + let mut items: Vec> = Vec::new(); + + for (i, (label, n)) in passos.iter().enumerate() { + let t = if *n == passo_ativo { + // Active step: bold-weight via larger size + primary color + text(*label).size(15).color(iced::Color::from_rgb(0.15, 0.5, 0.9)) + } else if *n < passo_ativo { + // Completed step: muted color + text(*label).size(14).color(iced::Color::from_rgb(0.5, 0.5, 0.5)) + } else { + // Future step: even more muted + text(*label).size(14).color(iced::Color::from_rgb(0.6, 0.6, 0.6)) + }; + items.push(t.into()); + if i < passos.len() - 1 { + items.push(text(" › ").size(14).color(iced::Color::from_rgb(0.5, 0.5, 0.5)).into()); + } + } + + container( + Row::with_children(items) + .align_y(Alignment::Center), + ) + .padding([4, 8]) + .width(Length::Fill) + .into() + } + + // ─── Lógica de background tasks ───────────────────────────────────────── + + fn processar_arquivo_selecionado(&mut self, caminho: PathBuf) -> Task { + let extensao = caminho + .extension() + .and_then(|e| e.to_str()) + .unwrap_or("") + .to_lowercase(); + + self.nome_arquivo = caminho + .file_name() + .map(|n| n.to_string_lossy().to_string()) + .unwrap_or_default(); + + match extensao.as_str() { + "csv" => { + self.tipo_arquivo_atual = TipoArquivo::Csv; + self.caminho_arquivo = Some(caminho.clone()); + self.notas_importadas.clear(); + let delim = self.layout_csv_atual.delimitador as u8; + let enc = self.layout_csv_atual.encoding.clone(); + self.preview_arquivo = + crate::infrastructure::csv_reader::preview_csv(&caminho, delim, &enc, 5).ok(); + Task::none() + } + "xlsx" | "xls" => { + self.tipo_arquivo_atual = TipoArquivo::Xlsx; + let layout_xlsx = self.layout_xlsx_atual.clone(); + let nome_layout = self.nome_layout_atual.clone(); + Task::perform( + async move { + tokio::task::spawn_blocking(move || { + listar_abas_xlsx(&caminho).map(|info| (caminho, info, layout_xlsx, nome_layout)) + }) + .await + .map_err(|e| e.to_string()) + .and_then(|r| r.map_err(|e| e.to_string())) + }, + |result| match result { + Ok((caminho, info, layout_xlsx, nome_layout)) => { + Message::AbaxlsxCarregadas { caminho, abas: info.abas, layout_xlsx, nome_layout } + } + Err(e) => Message::XlsxErroAoCarregar(e), + }, + ) + } + _ => { + self.exibir_erro("Formato de arquivo não suportado. Use CSV, XLSX ou XLS."); + Task::none() } } } - /// Processa o resultado recebido do canal de análise em background. - pub fn processar_resultado_pendente(&mut self, resultado: ResultadoPendente) { + fn disparar_importacao(&mut self) -> Task { + let caminho = match &self.caminho_arquivo { + Some(p) => p.clone(), + None => return Task::none(), + }; + + let tipo = self.tipo_arquivo_atual.clone(); + let layout_csv = self.layout_csv_atual.clone(); + let layout_xlsx = self.layout_xlsx_atual.clone(); + + self.estado = EstadoApp::Analisando; + + Task::perform( + async move { + tokio::task::spawn_blocking(move || executar_importacao_sync(caminho, tipo, layout_csv, layout_xlsx)) + .await + .unwrap_or_else(|e| ResultadoPendente::Erro(e.to_string())) + }, + Message::AnaliseCompleta, + ) + } + + fn reimportar_e_analisar(&mut self) -> Task { + let caminho = match &self.caminho_arquivo { + Some(p) => p.clone(), + None => { + self.exibir_erro("Nenhum arquivo carregado."); + return Task::none(); + } + }; + + if let EstadoApp::ExibindoResultado(r) = &self.estado { + self.resultado_anterior = Some(r.clone()); + } + + let tipo = self.tipo_arquivo_atual.clone(); + let layout_csv = self.layout_csv_atual.clone(); + let layout_xlsx = self.layout_xlsx_atual.clone(); + + self.estado = EstadoApp::Analisando; + + Task::perform( + async move { + tokio::task::spawn_blocking(move || executar_importacao_sync(caminho, tipo, layout_csv, layout_xlsx)) + .await + .unwrap_or_else(|e| ResultadoPendente::Erro(e.to_string())) + }, + Message::AnaliseCompleta, + ) + } + + fn processar_resultado_pendente(&mut self, resultado: ResultadoPendente) { match resultado { - ResultadoPendente::Concluido { - resultado, - avisos, - notas, - } => { + ResultadoPendente::Concluido { resultado, avisos, notas } => { if let Some(n) = notas { self.notas_importadas = n; } - if let Some(av) = &avisos { - self.avisos_importacao = if av.tem_avisos() { - Some(av.clone()) - } else { - None - }; - } + self.avisos_importacao = avisos.as_ref().filter(|a| a.tem_avisos()).cloned(); self.resultado_anterior = None; self.estado = EstadoApp::ExibindoResultado(resultado); if let Some(av) = &self.avisos_importacao.clone() { @@ -463,11 +878,7 @@ impl App { notas, } => { self.notas_importadas = notas; - self.avisos_importacao = if avisos.tem_avisos() { - Some(avisos) - } else { - None - }; + self.avisos_importacao = if avisos.tem_avisos() { Some(avisos) } else { None }; let msg = series_excessivas .iter() .map(|(chave, count)| { @@ -508,195 +919,298 @@ impl App { } } - /// Reimporta o arquivo atual com o layout atual e executa análise. - /// Segue o mesmo padrão assíncrono de `executar_importacao` em configuracao_colunas.rs. - pub fn reimportar_e_analisar(&mut self, ctx: &Context) { - let caminho = match &self.caminho_arquivo { - Some(p) => p.clone(), - None => { - self.exibir_erro("Nenhum arquivo carregado."); - return; + fn executar_acao_modal(&mut self, acao: AcaoModal) -> Task { + match acao { + AcaoModal::ConfirmarExpansaoFaltantes => { + self.update(Message::ConfirmarExpansaoFaltantes) } - }; - - // Preservar resultado atual para restaurar em caso de erro ou arquivo vazio - if let EstadoApp::ExibindoResultado(r) = &self.estado { - self.resultado_anterior = Some(r.clone()); - } - - let tipo = self.tipo_arquivo_atual.clone(); - let layout_csv = self.layout_csv_atual.clone(); - let layout_xlsx = self.layout_xlsx_atual.clone(); - - let (tx, rx) = mpsc::channel(); - self.resultado_pendente = Some(rx); - self.estado = EstadoApp::Analisando; - ctx.request_repaint(); - - std::thread::spawn(move || { - let res_importacao = match tipo { - TipoArquivo::Csv => importar_csv(&caminho, &layout_csv).map_err(|e| e.to_string()), - TipoArquivo::Xlsx => { - importar_xlsx(&caminho, &layout_xlsx).map_err(|e| e.to_string()) + AcaoModal::ConfirmarExclusaoLayout(id) => { + self.update(Message::ExclusaoConfirmada(id)) + } + AcaoModal::SobrescreverLayout(layout) => { + self.update(Message::SobrescreverLayout(layout)) + } + AcaoModal::ConfirmarNovaAnalise => { + self.notas_importadas.clear(); + self.preview_arquivo = None; + self.estado = EstadoApp::Importando; + Task::none() + } + AcaoModal::SalvarLayoutConfig => { + let texto = if let Some(EstadoModal::InputTexto { texto, .. }) = &self.modal { + texto.clone() + } else { + String::new() + }; + self.modal = None; + let nome = texto.trim().to_string(); + if nome.is_empty() { + self.exibir_erro("O nome do layout não pode ser vazio."); + return Task::none(); } - }; - - let res = match res_importacao { - Err(e) => ResultadoPendente::Erro(e), - Ok(importado) => { - if importado.notas.is_empty() { - ResultadoPendente::Vazio - } else { - let avisos = importado.avisos.clone(); - let notas = importado.notas; - - let pre = pre_analisar(¬as); - let excessivos = series_com_intervalo_excessivo(&pre); - - if !excessivos.is_empty() { - ResultadoPendente::AguardandoConfirmacao { - pre, - series_excessivas: excessivos, - avisos, - notas, - } - } else { - let resultado = expandir_analise(pre, ¬as); - ResultadoPendente::Concluido { - resultado, - avisos: Some(avisos), - notas: Some(notas), + let layout = match &self.tipo_arquivo_atual { + TipoArquivo::Csv => Layout::Csv { + id: None, + nome: nome.clone(), + config: self.layout_csv_atual.clone(), + }, + TipoArquivo::Xlsx => Layout::Xlsx { + id: None, + nome: nome.clone(), + config: self.layout_xlsx_atual.clone(), + }, + }; + let conn_arc = self.conn.as_ref().map(Arc::clone); + if let Some(conn_arc) = conn_arc { + let conn = conn_arc.lock().unwrap(); + match salvar_layout(&conn, &layout) { + Ok(_) => { + self.nome_layout_atual = nome; + drop(conn); + let cmd = self.recarregar_layouts(); + self.exibir_aviso("Layout salvo", "Layout salvo com sucesso."); + return cmd; + } + Err(ErroLayout::NomeConflitante(nome_conflito)) => { + let id_existente = self + .layouts_salvos + .iter() + .find(|l| l.nome() == nome_conflito) + .and_then(|l| l.id()); + drop(conn); + if let Some(id) = id_existente { + let tipo = self.tipo_arquivo_atual.clone(); + let layout_com_id = match tipo { + TipoArquivo::Csv => Layout::Csv { + id: Some(id), + nome: nome_conflito.clone(), + config: self.layout_csv_atual.clone(), + }, + TipoArquivo::Xlsx => Layout::Xlsx { + id: Some(id), + nome: nome_conflito.clone(), + config: self.layout_xlsx_atual.clone(), + }, + }; + self.exibir_confirmacao( + "Conflito de nome", + format!( + "Já existe um layout com o nome '{}'. Deseja sobrescrever?", + nome_conflito + ), + AcaoModal::SobrescreverLayout(layout_com_id), + ); } } + Err(e) => { + drop(conn); + self.exibir_erro(e.to_string()); + } } } - }; - - let _ = tx.send(res); - }); + Task::none() + } + } } - /// Executa a análise com as notas importadas. - pub fn executar_analise(&mut self) { - if self.notas_importadas.is_empty() { - self.exibir_aviso("Aviso", "Nenhuma nota importada para analisar."); - return; - } - - let pre = pre_analisar(&self.notas_importadas); - let excessivos = series_com_intervalo_excessivo(&pre); - - if !excessivos.is_empty() { - let msg = excessivos - .iter() - .map(|(chave, count)| { - format!( - "Série {}: intervalo de {} faltantes detectado", - chave.label(), - count - ) - }) - .collect::>() - .join("\n"); - - let acao = AcaoModal::ConfirmarExpansaoFaltantes; - self.estado = EstadoApp::ConfirmandoIntervalo { pre }; - self.exibir_confirmacao( - "Intervalo muito grande", - format!( - "As seguintes séries têm um número muito alto de faltantes:\n\n{}\n\nDeseja continuar mesmo assim?", - msg - ), - acao, - ); + fn exportar_pdf(&self) -> Task { + let resultado = match &self.estado { + EstadoApp::ExibindoResultado(r) => r.clone(), + _ => return Task::none(), + }; + let nome_arquivo = self.nome_arquivo.clone(); + let nome_layout = if self.nome_layout_atual.is_empty() { + None } else { - let resultado = expandir_analise(pre, &self.notas_importadas); - self.estado = EstadoApp::ExibindoResultado(resultado); - } - } -} - -impl App { - /// Renderiza o breadcrumb de etapas no topo (exceto na tela de layouts). - pub fn renderizar_breadcrumb(&self, ctx: &Context) { - let passo_ativo: usize = match self.tela_atual() { - 0 | 1 => 1, - 2 | 4 | 6 => 2, - 3 => 3, - _ => return, // GerenciandoLayouts: sem breadcrumb + Some(self.nome_layout_atual.clone()) }; - egui::TopBottomPanel::top("breadcrumb").show(ctx, |ui| { - ui.add_space(4.0); - ui.horizontal(|ui| { - for (i, label) in ["Arquivo", "Colunas", "Resultado"].iter().enumerate() { - let n = i + 1; - let texto_completo = format!("{}. {}", n, label); - let texto = egui::RichText::new(texto_completo); - if n == passo_ativo { - ui.label(texto.strong()); - } else if n < passo_ativo { - ui.label(texto); - } else { - ui.label(texto.weak()); - } - if n < 3 { - ui.label(egui::RichText::new(" ›").weak()); - } - } - }); - ui.add_space(4.0); - }); - } -} + Task::perform( + async move { + let dest = rfd::AsyncFileDialog::new() + .set_file_name("relatorio.pdf") + .add_filter("PDF", &["pdf"]) + .save_file() + .await + .map(|h| h.path().to_path_buf()); -impl eframe::App for App { - fn update(&mut self, ctx: &Context, _frame: &mut eframe::Frame) { - // Verificar resultado pendente da análise em background - if let Some(rx) = self.resultado_pendente.take() { - match rx.try_recv() { - Ok(resultado) => { - self.processar_resultado_pendente(resultado); + let caminho = match dest { + Some(p) => p, + None => return Ok(std::path::PathBuf::new()), // cancelado + }; + + tokio::task::spawn_blocking(move || { + use crate::application::usecases::exportar_pdf::exportar_pdf; + use crate::infrastructure::pdf_generator::GenpdfGenerator; + let gerador = GenpdfGenerator; + exportar_pdf( + &gerador, + &resultado, + &nome_arquivo, + nome_layout.as_deref(), + &caminho, + ) + .map(|_| caminho) + .map_err(|e| e.to_string()) + }) + .await + .map_err(|e| e.to_string()) + .and_then(|r| r) + }, + |result| match result { + Ok(caminho) if caminho.as_os_str().is_empty() => Message::Noop, + Ok(caminho) => Message::PdfExportado(Ok(caminho)), + Err(e) => Message::PdfExportado(Err(e)), + }, + ) + } + + fn exportar_layout_json(&self, id: i64) -> Task { + let layout = match self.layouts_salvos.iter().find(|l| l.id() == Some(id)) { + Some(l) => l.clone(), + None => return Task::none(), + }; + + let (conteudo, nome_sugerido) = match exportar_layout_json(&layout) { + Ok(r) => r, + Err(e) => { + // Não temos &mut self aqui; retornamos um command que exibe erro + return Task::perform(async move { e.to_string() }, |e| { + Message::PdfExportado(Err(e)) + }); + } + }; + + Task::perform( + async move { + let dest = rfd::AsyncFileDialog::new() + .set_file_name(&nome_sugerido) + .add_filter("JSON", &["json"]) + .save_file() + .await + .map(|h| h.path().to_path_buf()); + + if let Some(caminho) = dest { + tokio::fs::write(&caminho, conteudo) + .await + .map(|_| caminho) + .map_err(|e| e.to_string()) + } else { + Err(String::new()) // cancelado } - Err(mpsc::TryRecvError::Empty) => { - // Ainda processando: devolver receiver e solicitar repaint - self.resultado_pendente = Some(rx); - ctx.request_repaint(); + }, + |result| match result { + Ok(_) => Message::Noop, + Err(e) if e.is_empty() => Message::Noop, + Err(e) => Message::PdfExportado(Err(format!("Erro ao salvar JSON: {}", e))), + }, + ) + } + + fn processar_importacao_json(&mut self, conteudo: &str) -> Task { + let conn_arc = self.conn.as_ref().map(Arc::clone); + if let Some(conn_arc) = conn_arc { + let conn = conn_arc.lock().unwrap(); + match importar_layout_json(&conn, conteudo, false, None) { + Ok(_) => { + drop(conn); + let cmd = self.recarregar_layouts(); + self.exibir_aviso("Sucesso", "Layout importado com sucesso."); + return cmd; } - Err(mpsc::TryRecvError::Disconnected) => { - self.estado = EstadoApp::ConfigurandoColunas; - self.exibir_erro("Erro interno: a análise foi interrompida inesperadamente."); + Err(ErroLayout::NomeConflitante(nome)) => { + let parsed = serde_json::from_str::(conteudo) + .ok() + .and_then(|json_repr| Layout::try_from(json_repr).ok()); + + let id_existente = self + .layouts_salvos + .iter() + .find(|l| l.nome() == nome) + .and_then(|l| l.id()); + + drop(conn); + + match (parsed, id_existente) { + (Some(mut layout), Some(id)) => { + match &mut layout { + Layout::Csv { id: i, .. } => *i = Some(id), + Layout::Xlsx { id: i, .. } => *i = Some(id), + } + self.exibir_confirmacao( + "Conflito de nome", + format!( + "Já existe um layout com o nome '{}'. Deseja sobrescrever?", + nome + ), + AcaoModal::SobrescreverLayout(layout), + ); + } + _ => { + self.exibir_erro(format!( + "Conflito de nome: layout '{}' já existe.", + nome + )); + } + } + } + Err(e) => { + drop(conn); + self.exibir_erro(format!("Erro ao importar layout: {}", e)); + } + } + } + Task::none() + } +} + +// ─── Mensagens extras para o fluxo XLSX ───────────────────────────────────── +// (precisamos adicionar estas variantes ao enum Message) + +// Nota: as variantes AbaxlsxCarregadas e XlsxErroAoCarregar serão tratadas +// no match do update() principal via um segundo match arm de Message. +// Para isso, precisamos adicionar estas variantes ao message.rs também. + +// ─── Função auxiliar síncrona para executar importação em spawn_blocking ───── + +fn executar_importacao_sync( + caminho: PathBuf, + tipo: TipoArquivo, + layout_csv: LayoutCsv, + layout_xlsx: LayoutXlsx, +) -> ResultadoPendente { + let res_importacao = match tipo { + TipoArquivo::Csv => importar_csv(&caminho, &layout_csv).map_err(|e| e.to_string()), + TipoArquivo::Xlsx => importar_xlsx(&caminho, &layout_xlsx).map_err(|e| e.to_string()), + }; + + match res_importacao { + Err(e) => ResultadoPendente::Erro(e), + Ok(importado) => { + if importado.notas.is_empty() { + ResultadoPendente::Vazio + } else { + let avisos = importado.avisos.clone(); + let notas = importado.notas; + let pre = pre_analisar(¬as); + let excessivos = series_com_intervalo_excessivo(&pre); + + if !excessivos.is_empty() { + ResultadoPendente::AguardandoConfirmacao { + pre, + series_excessivas: excessivos, + avisos, + notas, + } + } else { + let resultado = expandir_analise(pre, ¬as); + ResultadoPendente::Concluido { + resultado, + avisos: Some(avisos), + notas: Some(notas), + } } } } - - // Sempre renderizar modal por cima de tudo - self.renderizar_modal(ctx); - - // Breadcrumb de etapas no topo - self.renderizar_breadcrumb(ctx); - - // Determinar qual tela exibir sem borrar self.estado - let tela_id = self.tela_atual(); - - // Renderizar a tela atual - egui::CentralPanel::default().show(ctx, |ui| match tela_id { - 0 => crate::ui::screens::import::renderizar(ui, ctx, self), - 1 => crate::ui::screens::import::renderizar_selecao_aba(ui, ctx, self), - 2 => crate::ui::screens::configuracao_colunas::renderizar(ui, ctx, self), - 3 => crate::ui::screens::resultado::renderizar(ui, ctx, self), - 4 => crate::ui::screens::configuracao_colunas::renderizar(ui, ctx, self), - 5 => crate::ui::screens::layouts::renderizar(ui, ctx, self), - 6 => { - ui.centered_and_justified(|ui| { - ui.label( - egui::RichText::new("⏳ Analisando... aguarde.") - .size(22.0) - .strong(), - ); - }); - } - _ => {} - }); } } diff --git a/src/ui/components/mod.rs b/src/ui/components/mod.rs new file mode 100644 index 0000000..22464fd --- /dev/null +++ b/src/ui/components/mod.rs @@ -0,0 +1,3 @@ +pub mod modal; +pub mod paginacao; +pub mod tabela_preview; diff --git a/src/ui/components/modal.rs b/src/ui/components/modal.rs new file mode 100644 index 0000000..3e78bfb --- /dev/null +++ b/src/ui/components/modal.rs @@ -0,0 +1,85 @@ +use crate::ui::app::EstadoModal; +use crate::ui::message::Message; +use iced::widget::{button, column, container, mouse_area, row, stack, text, text_input}; +use iced::{Alignment, Color, Element, Length}; + +/// Envolve o conteúdo principal com uma camada de modal por cima. +/// O overlay escuro bloqueia cliques no conteúdo de baixo. +pub fn view_com_modal<'a>( + conteudo: Element<'a, Message>, + modal: &'a EstadoModal, +) -> Element<'a, Message> { + let overlay = mouse_area( + container(view_modal(modal)) + .width(Length::Fill) + .height(Length::Fill) + .style(|_theme| container::Style { + background: Some(Color::from_rgba(0.0, 0.0, 0.0, 0.5).into()), + ..Default::default() + }) + .center_x(Length::Fill) + .center_y(Length::Fill), + ) + .on_press(Message::ModalCancelado); + + stack![conteudo, overlay].into() +} + +fn view_modal(modal: &EstadoModal) -> Element<'_, Message> { + match modal { + EstadoModal::Informacao { titulo, mensagem } => caixa_modal(titulo, mensagem, None, false), + EstadoModal::Aviso { titulo, mensagem } => caixa_modal(titulo, mensagem, None, false), + EstadoModal::Erro { titulo, mensagem } => caixa_modal(titulo, mensagem, None, false), + EstadoModal::Confirmacao { + titulo, mensagem, .. + } => caixa_modal(titulo, mensagem, None, true), + EstadoModal::InputTexto { + titulo, + mensagem, + texto, + .. + } => caixa_modal(titulo, mensagem, Some(texto.as_str()), true), + } +} + +fn caixa_modal<'a>( + titulo: &'a str, + mensagem: &'a str, + input: Option<&'a str>, + com_confirmar: bool, +) -> Element<'a, Message> { + let mut col = column![text(titulo).size(18), text(mensagem).size(14),].spacing(8); + + if let Some(valor) = input { + col = col.push( + text_input("Nome...", valor) + .on_input(Message::ModalTextoAlterado) + .padding(6), + ); + } + + let mut botoes = row![button("Fechar").on_press(Message::ModalCancelado),].spacing(8); + + if com_confirmar { + botoes = botoes.push(button("Confirmar").on_press(Message::ModalConfirmado)); + } + + col = col.push(botoes); + + container(col.align_x(Alignment::Start)) + .width(Length::Fixed(400.0)) + .padding(24) + .style(|theme: &iced::Theme| { + let palette = theme.extended_palette(); + container::Style { + background: Some(palette.background.base.color.into()), + border: iced::Border { + color: palette.background.strong.color, + width: 1.0, + radius: 8.0.into(), + }, + ..Default::default() + } + }) + .into() +} diff --git a/src/ui/components/paginacao.rs b/src/ui/components/paginacao.rs new file mode 100644 index 0000000..6b8bc09 --- /dev/null +++ b/src/ui/components/paginacao.rs @@ -0,0 +1,23 @@ +use crate::ui::message::Message; +use iced::widget::{button, row, text}; +use iced::Element; + +/// Renderiza controles de paginação reutilizáveis. +pub fn controles_paginacao( + pagina_atual: usize, + total_paginas: usize, + msg_anterior: Message, + msg_proximo: Message, +) -> Element<'static, Message> { + let btn_anterior = button("◀").on_press_maybe((pagina_atual > 0).then_some(msg_anterior)); + let btn_proximo = + button("▶").on_press_maybe((pagina_atual + 1 < total_paginas).then_some(msg_proximo)); + + row![ + btn_anterior, + text(format!("Página {} / {}", pagina_atual + 1, total_paginas)).size(13), + btn_proximo, + ] + .spacing(8) + .into() +} diff --git a/src/ui/components/tabela_preview.rs b/src/ui/components/tabela_preview.rs new file mode 100644 index 0000000..2f6600f --- /dev/null +++ b/src/ui/components/tabela_preview.rs @@ -0,0 +1,58 @@ +use crate::ui::message::Message; +use crate::ui::screens::indice_para_letra; +use iced::widget::{column, row, scrollable, text}; +use iced::{Element, Font, Length}; + +/// Renderiza uma tabela de pré-visualização das primeiras linhas do arquivo. +/// Mostra uma linha de cabeçalho com letras estilo Excel (A, B, C, ...) seguida pelos dados. +pub fn tabela_preview(linhas: &[Vec]) -> Element<'_, Message> { + let num_colunas = linhas.iter().map(|l| l.len()).max().unwrap_or(0); + if num_colunas == 0 { + return text("(vazio)").size(12).into(); + } + + let cabecalho = row((0..num_colunas) + .map(|i| { + text(format!("{} ({})", indice_para_letra(i), i)) + .font(Font::MONOSPACE) + .size(12) + .width(Length::Fixed(120.0)) + .into() + }) + .collect::>()) + .spacing(4); + + let linhas_view = linhas.iter().map(|linha| { + row((0..num_colunas) + .map(|col| { + let celula = linha.get(col).map(|s| s.as_str()).unwrap_or(""); + let truncado: String = if celula.chars().count() > 30 { + format!("{}...", celula.chars().take(30).collect::()) + } else { + celula.to_string() + }; + text(truncado) + .font(Font::MONOSPACE) + .size(11) + .width(Length::Fixed(120.0)) + .into() + }) + .collect::>()) + .spacing(4) + .into() + }); + + let todas_linhas = column( + std::iter::once(cabecalho.into()) + .chain(linhas_view) + .collect::>(), + ) + .spacing(2); + + scrollable(todas_linhas) + .direction(scrollable::Direction::Horizontal( + scrollable::Scrollbar::default(), + )) + .height(Length::Fixed(160.0)) + .into() +} diff --git a/src/ui/message.rs b/src/ui/message.rs new file mode 100644 index 0000000..b1241c4 --- /dev/null +++ b/src/ui/message.rs @@ -0,0 +1,122 @@ +use crate::domain::entities::nota::Nota; +use crate::domain::entities::{ + chave_serie::ChaveSerie, + layout::{Layout, LayoutXlsx}, + resultado_analise::{ResultadoAnalise, ResultadoPreAnalise}, +}; +use crate::domain::errors::ResumoAvisos; +use rusqlite::Connection; +use std::path::PathBuf; +use std::sync::{Arc, Mutex}; + +/// Todos os eventos/interações da UI. +#[derive(Debug, Clone)] +pub enum Message { + // --- Inicialização --- + BancoInicializado(Result<(Arc>, bool, Vec), String>), + LayoutsRecarregados(Vec), + + // --- Navegação --- + IrParaImportacao, + IrParaConfiguracaoColunas, + IrParaLayouts, + Voltar, + + // --- Arquivo --- + SelecionarArquivo, + ArquivoSelecionado(PathBuf), + AbaSelecionada(String), + + // --- XLSX: abas carregadas em background --- + AbaxlsxCarregadas { + caminho: PathBuf, + abas: Vec, + layout_xlsx: LayoutXlsx, + nome_layout: String, + }, + XlsxErroAoCarregar(String), + + // --- Background tasks --- + AnaliseCompleta(ResultadoPendente), + + // --- Configuração CSV --- + DelimitadorAlterado(char), + EncodingAlterado(String), + LinhaCabecalhoAlterada(usize), + IndiceNumeroAlterado(usize), + IndiceSerieAlterado(usize), + IndiceValorToggle(bool), + IndiceValorAlterado(usize), + IndiceDataToggle(bool), + IndiceDataAlterado(usize), + IndiceDocTipoToggle(bool), + IndiceDocTipoAlterado(usize), + + // --- Configuração XLSX --- + AbaXlsxAlterada(String), + PosNumeroAlterada(String), + PosSerieAlterada(String), + PosValorToggle(bool), + PosValorAlterada(String), + PosDataToggle(bool), + PosDataAlterada(String), + PosDocTipoToggle(bool), + PosDocTipoAlterada(String), + + // --- Análise --- + ExecutarImportacao, + ReanalisarArquivo, + ConfirmarExpansaoFaltantes, + CancelarExpansao, + NovaAnalise, + + // --- Resultado --- + PaginaFaltantesAlterada(usize), + PaginaDuplicatasAlterada(usize), + ItensPorPaginaAlterado(usize), + CopiarFaltantes(ChaveSerie), + CopiarDuplicatas(ChaveSerie), + ExportarPdf, + PdfExportado(Result), + + // --- Layouts --- + LayoutSelecionado(i64), + SalvarLayout, + NomeLayoutAlterado(String), + ExcluirLayout(i64), + ExclusaoConfirmada(i64), + ExportarLayoutJson(i64), + ImportarLayoutJson, + LayoutJsonImportado(String), + SobrescreverLayout(Layout), + + // --- Modal --- + ModalTextoAlterado(String), + ModalConfirmado, + ModalCancelado, + + // --- Sem operação (used as fallback) --- + Noop, +} + +/// Resultado enviado pela task de análise em background para a UI. +#[derive(Debug, Clone)] +pub enum ResultadoPendente { + /// Análise concluída com sucesso. + Concluido { + resultado: ResultadoAnalise, + avisos: Option, + notas: Option>, + }, + /// Pré-análise concluída mas precisa de confirmação do usuário. + AguardandoConfirmacao { + pre: ResultadoPreAnalise, + series_excessivas: Vec<(ChaveSerie, u64)>, + avisos: ResumoAvisos, + notas: Vec, + }, + /// Arquivo importado não continha notas válidas. + Vazio, + /// Erro durante importação ou análise. + Erro(String), +} diff --git a/src/ui/mod.rs b/src/ui/mod.rs index 028e0f2..54e2e99 100644 --- a/src/ui/mod.rs +++ b/src/ui/mod.rs @@ -1,2 +1,4 @@ pub mod app; +pub mod components; +pub mod message; pub mod screens; diff --git a/src/ui/screens/configuracao_colunas.rs b/src/ui/screens/configuracao_colunas.rs index c563715..2adc136 100644 --- a/src/ui/screens/configuracao_colunas.rs +++ b/src/ui/screens/configuracao_colunas.rs @@ -1,336 +1,379 @@ -use crate::application::usecases::executar_analise::{ - expandir_analise, pre_analisar, series_com_intervalo_excessivo, +use crate::domain::entities::layout::TipoArquivo; +use crate::ui::app::App; +use crate::ui::message::Message; +use iced::widget::{ + button, checkbox, column, container, pick_list, row, scrollable, text, text_input, Column, }; -use crate::application::usecases::importar_arquivo::{importar_csv, importar_xlsx}; -use crate::domain::entities::layout::{Layout, TipoArquivo}; -use crate::ui::app::{App, EstadoApp, ResultadoPendente}; -use egui::{Context, Ui}; +use iced::{Alignment, Element, Length}; -/// Renderiza a tela de configuração de colunas. -pub fn renderizar(ui: &mut Ui, ctx: &Context, app: &mut App) { - ui.heading("Configuração de Colunas"); - ui.add_space(8.0); +const OPCOES_DELIMITADOR: &[(&str, char)] = &[ + ("Vírgula (,)", ','), + ("Ponto e vírgula (;)", ';'), + ("Tabulação (Tab)", '\t'), +]; - if let Some(caminho) = &app.caminho_arquivo.clone() { - ui.label(format!("Arquivo: {}", caminho.display())); - } +const OPCOES_ENCODING: &[&str] = &["utf-8", "windows-1252"]; - ui.add_space(8.0); +/// Tela de configuração de colunas. +pub fn view(app: &App) -> Element<'_, Message> { + let titulo = text("Configuração de Colunas").size(22); + + let arquivo_label = if let Some(caminho) = &app.caminho_arquivo { + text(format!("Arquivo: {}", caminho.display())).size(13) + } else { + text("").size(13) + }; // Seletor de layout let tipo_atual = app.tipo_arquivo_atual.clone(); - let opcoes_layout: Vec<(i64, String)> = app + let opcoes_layout: Vec = app .layouts_salvos .iter() .filter(|l| l.tipo() == tipo_atual) - .filter_map(|l| l.id().map(|id| (id, l.nome().to_string()))) + .map(|l| l.nome().to_string()) .collect(); - let nome_layout_atual = app.nome_layout_atual.clone(); + let nome_layout_sel: Option = if app.nome_layout_atual.is_empty() { + None + } else { + Some(app.nome_layout_atual.clone()) + }; - ui.horizontal(|ui| { - ui.label("Layout:"); - egui::ComboBox::from_id_salt("combo_layouts_config") - .selected_text(if nome_layout_atual.is_empty() { - "— Selecionar layout —" - } else { - &nome_layout_atual - }) - .show_ui(ui, |ui| { - for (id, nome) in &opcoes_layout { - if ui - .selectable_label(nome_layout_atual == *nome, nome.as_str()) - .clicked() - { - app.nome_layout_atual = nome.clone(); - if let Some(layout) = - app.layouts_salvos.iter().find(|l| l.id() == Some(*id)) - { - let layout = layout.clone(); - match &layout { - Layout::Csv { config, .. } => { - app.layout_csv_atual = config.clone(); - } - Layout::Xlsx { config, .. } => { - app.layout_xlsx_atual = config.clone(); - } - } - } - } + let secao_layout = row![ + text("Layout:").size(14), + pick_list(opcoes_layout, nome_layout_sel, { + let layouts = app.layouts_salvos.clone(); + move |nome_selecionado: String| { + // Busca o id do layout pelo nome e dispara LayoutSelecionado + if let Some(id) = layouts + .iter() + .find(|l| l.nome() == nome_selecionado) + .and_then(|l| l.id()) + { + Message::LayoutSelecionado(id) + } else { + Message::NomeLayoutAlterado(nome_selecionado) } - }); - }); + } + }) + .placeholder("— Selecionar layout —") + .width(Length::Fixed(240.0)), + ] + .spacing(8) + .align_y(Alignment::Center); - ui.add_space(12.0); + // Configuração específica por tipo + let config_section = match app.tipo_arquivo_atual { + TipoArquivo::Csv => view_csv(app), + TipoArquivo::Xlsx => view_xlsx(app), + }; - match app.tipo_arquivo_atual.clone() { - TipoArquivo::Csv => renderizar_csv(ui, app), - TipoArquivo::Xlsx => renderizar_xlsx(ui, app), - } + // Pré-visualização + let preview_section: Element = if let Some(linhas) = &app.preview_arquivo { + column![ + text("Pré-visualização:").size(13), + crate::ui::components::tabela_preview::tabela_preview(linhas), + ] + .spacing(4) + .into() + } else { + text("").into() + }; - // Pré-visualização do arquivo - if let Some(preview) = &app.preview_arquivo.clone() { - ui.add_space(8.0); - ui.separator(); - ui.add_space(4.0); - crate::ui::screens::renderizar_tabela_preview(ui, preview); - } - - ui.add_space(16.0); - ui.separator(); - ui.add_space(8.0); - - // Validação e botões de ação + // Validação let (valido, erros) = validar_config(app); - if !erros.is_empty() { - for erro in &erros { - ui.colored_label(egui::Color32::RED, format!("⚠ {}", erro)); - } - ui.add_space(8.0); + let erros_section: Element = if erros.is_empty() { + text("").into() + } else { + Column::with_children( + erros + .iter() + .map(|e| text(format!("⚠ {}", e)).size(13).into()) + .collect::>(), + ) + .spacing(2) + .into() + }; + + let tem_arquivo = app.caminho_arquivo.is_some(); + let tem_notas = !app.notas_importadas.is_empty(); + + let botoes = row![ + button("< Voltar").on_press(Message::Voltar), + button("▶ Importar e Analisar") + .on_press_maybe((valido && tem_arquivo).then_some(Message::ExecutarImportacao)), + button("🔄 Reanalisar") + .on_press_maybe((valido && tem_notas).then_some(Message::ReanalisarArquivo)), + button("💾 Salvar como layout...").on_press(Message::SalvarLayout), + ] + .spacing(8); + + let content = column![ + titulo, + arquivo_label, + secao_layout, + config_section, + preview_section, + erros_section, + botoes, + ] + .spacing(12) + .padding(16) + .width(Length::Fill); + + container(scrollable(content)) + .width(Length::Fill) + .height(Length::Fill) + .into() +} + +fn view_csv(app: &App) -> Element<'_, Message> { + let c = &app.layout_csv_atual; + + let delim_str = match c.delimitador { + ',' => "Vírgula (,)", + ';' => "Ponto e vírgula (;)", + '\t' => "Tabulação (Tab)", + _ => "Outro", } + .to_string(); - ui.horizontal(|ui| { - if ui.button("< Voltar").clicked() { - app.estado = EstadoApp::Importando; - } + let opcoes_delim: Vec = OPCOES_DELIMITADOR + .iter() + .map(|(s, _)| s.to_string()) + .collect(); - ui.add_enabled_ui(valido && app.caminho_arquivo.is_some(), |ui| { - if ui.button("▶ Importar e Analisar").clicked() { - executar_importacao(app, ctx); - } - }); + let opcoes_enc: Vec = OPCOES_ENCODING.iter().map(|s| s.to_string()).collect(); - if valido && !app.notas_importadas.is_empty() { - if ui - .button("🔄 Reanalisar") - .on_hover_text("Reanalisa as notas já importadas sem reimportar o arquivo") - .clicked() - { - app.executar_analise(); - } - } + let linha_cabecalho_str = c.linha_cabecalho.to_string(); + let indice_numero_str = c.indice_numero.to_string(); + let indice_serie_str = c.indice_serie.to_string(); - if ui.button("💾 Salvar como layout...").clicked() { - app.exibir_modal_salvar_layout(); - } - }); -} - -fn renderizar_csv(ui: &mut Ui, app: &mut App) { - ui.group(|ui| { - ui.label("Configurações CSV"); - ui.add_space(4.0); - - // Delimitador - ui.horizontal(|ui| { - ui.label("Delimitador:"); - let delim_str = match app.layout_csv_atual.delimitador { - ',' => "Vírgula (,)", - ';' => "Ponto e vírgula (;)", - '\t' => "Tabulação (Tab)", - _ => "Outro", - }; - let mut delim_mudou = false; - egui::ComboBox::from_id_salt("combo_delimitador") - .selected_text(delim_str) - .show_ui(ui, |ui| { - if ui - .selectable_label(app.layout_csv_atual.delimitador == ',', "Vírgula (,)") - .clicked() - { - app.layout_csv_atual.delimitador = ','; - delim_mudou = true; - } - if ui - .selectable_label( - app.layout_csv_atual.delimitador == ';', - "Ponto e vírgula (;)", - ) - .clicked() - { - app.layout_csv_atual.delimitador = ';'; - delim_mudou = true; - } - if ui - .selectable_label( - app.layout_csv_atual.delimitador == '\t', - "Tabulação (Tab)", - ) - .clicked() - { - app.layout_csv_atual.delimitador = '\t'; - delim_mudou = true; - } - }); - if delim_mudou { - if let Some(caminho) = &app.caminho_arquivo.clone() { - app.preview_arquivo = crate::infrastructure::csv_reader::preview_csv( - caminho, - app.layout_csv_atual.delimitador as u8, - &app.layout_csv_atual.encoding.clone(), - 5, - ) - .ok(); - } - } - }); - - // Encoding - ui.horizontal(|ui| { - ui.label("Encoding:"); - egui::ComboBox::from_id_salt("combo_encoding") - .selected_text(&app.layout_csv_atual.encoding) - .show_ui(ui, |ui| { - if ui - .selectable_label(app.layout_csv_atual.encoding == "utf-8", "UTF-8") - .clicked() - { - app.layout_csv_atual.encoding = "utf-8".to_string(); - } - if ui - .selectable_label( - app.layout_csv_atual.encoding == "windows-1252", - "Windows-1252 (Latin-1)", - ) - .clicked() - { - app.layout_csv_atual.encoding = "windows-1252".to_string(); - } - }); - }); - - // Linha cabeçalho - ui.horizontal(|ui| { - ui.label("Linha do cabeçalho (0 = sem cabeçalho):"); - let mut val = app.layout_csv_atual.linha_cabecalho; - ui.add(egui::DragValue::new(&mut val).range(0..=100)); - app.layout_csv_atual.linha_cabecalho = val; - }); - }); - - ui.add_space(8.0); - ui.group(|ui| { - ui.label("Mapeamento de Colunas (índice base 0)"); - ui.add_space(4.0); - - campo_indice( - ui, - "Número (obrigatório):", - &mut app.layout_csv_atual.indice_numero, - ); - campo_indice( - ui, - "Série (obrigatório):", - &mut app.layout_csv_atual.indice_serie, - ); - - campo_indice_opcional( - ui, + column![ + text("Configurações CSV").size(16), + row![ + text("Delimitador:").size(14).width(Length::Fixed(220.0)), + pick_list(opcoes_delim, Some(delim_str), |selecionado| { + let c = OPCOES_DELIMITADOR + .iter() + .find(|(s, _)| *s == selecionado) + .map(|(_, c)| *c) + .unwrap_or(','); + Message::DelimitadorAlterado(c) + }) + .width(Length::Fixed(200.0)), + ] + .spacing(8) + .align_y(Alignment::Center), + row![ + text("Encoding:").size(14).width(Length::Fixed(220.0)), + pick_list( + opcoes_enc, + Some(c.encoding.clone()), + Message::EncodingAlterado + ) + .width(Length::Fixed(200.0)), + ] + .spacing(8) + .align_y(Alignment::Center), + row![ + text("Linha do cabeçalho:") + .size(14) + .width(Length::Fixed(220.0)), + text_input("0", &linha_cabecalho_str) + .on_input(|s| { + s.parse::() + .map(Message::LinhaCabecalhoAlterada) + .unwrap_or(Message::Noop) + }) + .width(Length::Fixed(80.0)), + ] + .spacing(8) + .align_y(Alignment::Center), + text("Mapeamento de Colunas (índice base 0)").size(14), + row![ + text("Número (obrigatório):") + .size(14) + .width(Length::Fixed(220.0)), + text_input("0", &indice_numero_str) + .on_input(|s| { + s.parse::() + .map(Message::IndiceNumeroAlterado) + .unwrap_or(Message::Noop) + }) + .width(Length::Fixed(80.0)), + ] + .spacing(8) + .align_y(Alignment::Center), + row![ + text("Série (obrigatório):") + .size(14) + .width(Length::Fixed(220.0)), + text_input("0", &indice_serie_str) + .on_input(|s| { + s.parse::() + .map(Message::IndiceSerieAlterado) + .unwrap_or(Message::Noop) + }) + .width(Length::Fixed(80.0)), + ] + .spacing(8) + .align_y(Alignment::Center), + campo_indice_opcional_csv( "Valor (opcional):", - &mut app.layout_csv_atual.indice_valor, - ); - campo_indice_opcional( - ui, + c.indice_valor, + Message::IndiceValorToggle, + Message::IndiceValorAlterado, + ), + campo_indice_opcional_csv( "Data (opcional):", - &mut app.layout_csv_atual.indice_data, - ); - campo_indice_opcional( - ui, + c.indice_data, + Message::IndiceDataToggle, + Message::IndiceDataAlterado, + ), + campo_indice_opcional_csv( "Tipo Documento (opcional):", - &mut app.layout_csv_atual.indice_documento_tipo, - ); - }); + c.indice_documento_tipo, + Message::IndiceDocTipoToggle, + Message::IndiceDocTipoAlterado, + ), + ] + .spacing(8) + .into() } -fn renderizar_xlsx(ui: &mut Ui, app: &mut App) { - ui.group(|ui| { - ui.label("Configurações XLSX"); - ui.add_space(4.0); +fn campo_indice_opcional_csv( + label: &str, + valor: Option, + msg_toggle: impl Fn(bool) -> Message + 'static, + msg_valor: impl Fn(usize) -> Message + 'static, +) -> Element<'static, Message> { + let ativo = valor.is_some(); + let val_str = valor.map(|v| v.to_string()).unwrap_or_default(); - ui.horizontal(|ui| { - ui.label("Aba:"); - if app.abas_xlsx.is_empty() { - ui.text_edit_singleline(&mut app.layout_xlsx_atual.aba); - } else { - let aba_atual = app.layout_xlsx_atual.aba.clone(); - egui::ComboBox::from_id_salt("combo_aba") - .selected_text(&aba_atual) - .show_ui(ui, |ui| { - for aba in &app.abas_xlsx.clone() { - if ui.selectable_label(aba_atual == *aba, aba).clicked() { - app.layout_xlsx_atual.aba = aba.clone(); - } - } - }); - } - }); - }); + let cb = checkbox(label, ativo).on_toggle(msg_toggle); - ui.add_space(8.0); - ui.group(|ui| { - ui.label("Mapeamento de Colunas (formato LetraLinha, ex: B3)"); - ui.add_space(4.0); + if ativo { + row![ + cb.width(Length::Fixed(220.0)), + text_input("0", &val_str) + .on_input(move |s| { s.parse::().map(&msg_valor).unwrap_or(Message::Noop) }) + .width(Length::Fixed(80.0)), + ] + .spacing(8) + .align_y(Alignment::Center) + .into() + } else { + row![cb].into() + } +} +fn view_xlsx(app: &App) -> Element<'_, Message> { + let c = &app.layout_xlsx_atual; + + let secao_aba: Element = if app.abas_xlsx.is_empty() { + row![ + text("Aba:").size(14).width(Length::Fixed(200.0)), + text_input("Nome da aba", &c.aba) + .on_input(Message::AbaXlsxAlterada) + .width(Length::Fixed(200.0)), + ] + .spacing(8) + .align_y(Alignment::Center) + .into() + } else { + let aba_sel = if c.aba.is_empty() { + None + } else { + Some(c.aba.clone()) + }; + row![ + text("Aba:").size(14).width(Length::Fixed(200.0)), + pick_list(app.abas_xlsx.clone(), aba_sel, Message::AbaXlsxAlterada) + .width(Length::Fixed(200.0)), + ] + .spacing(8) + .align_y(Alignment::Center) + .into() + }; + + column![ + text("Configurações XLSX").size(16), + secao_aba, + text("Mapeamento de Colunas (formato LetraLinha, ex: B3)").size(14), campo_letra_linha( - ui, "Número (obrigatório):", - &mut app.layout_xlsx_atual.pos_numero, - ); + &c.pos_numero, + Message::PosNumeroAlterada + ), campo_letra_linha( - ui, "Série (obrigatório):", - &mut app.layout_xlsx_atual.pos_serie, - ); + &c.pos_serie, + Message::PosSerieAlterada + ), campo_letra_linha_opcional( - ui, "Valor (opcional):", - &mut app.layout_xlsx_atual.pos_valor, - ); - campo_letra_linha_opcional(ui, "Data (opcional):", &mut app.layout_xlsx_atual.pos_data); + c.pos_valor.as_deref(), + Message::PosValorToggle, + Message::PosValorAlterada, + ), + campo_letra_linha_opcional( + "Data (opcional):", + c.pos_data.as_deref(), + Message::PosDataToggle, + Message::PosDataAlterada, + ), campo_letra_linha_opcional( - ui, "Tipo Documento (opcional):", - &mut app.layout_xlsx_atual.pos_documento_tipo, - ); - }); + c.pos_documento_tipo.as_deref(), + Message::PosDocTipoToggle, + Message::PosDocTipoAlterada, + ), + ] + .spacing(8) + .into() } -fn campo_indice(ui: &mut Ui, label: &str, valor: &mut usize) { - ui.horizontal(|ui| { - ui.label(label); - ui.add(egui::DragValue::new(valor).range(0..=999usize)); - }); +fn campo_letra_linha<'a>( + label: &'a str, + valor: &'a str, + msg: impl Fn(String) -> Message + 'a, +) -> Element<'a, Message> { + row![ + text(label).size(14).width(Length::Fixed(200.0)), + text_input("ex: B3", valor) + .on_input(msg) + .width(Length::Fixed(100.0)), + ] + .spacing(8) + .align_y(Alignment::Center) + .into() } -fn campo_indice_opcional(ui: &mut Ui, label: &str, valor: &mut Option) { - ui.horizontal(|ui| { - let mut ativo = valor.is_some(); - if ui.checkbox(&mut ativo, label).changed() { - *valor = if ativo { Some(0) } else { None }; - } - if let Some(v) = valor { - ui.add(egui::DragValue::new(v).range(0..=999usize)); - } - }); -} +fn campo_letra_linha_opcional<'a>( + label: &'a str, + valor: Option<&'a str>, + msg_toggle: impl Fn(bool) -> Message + 'a, + msg_valor: impl Fn(String) -> Message + 'a, +) -> Element<'a, Message> { + let ativo = valor.is_some(); + let val_str = valor.unwrap_or("").to_string(); -fn campo_letra_linha(ui: &mut Ui, label: &str, valor: &mut String) { - ui.horizontal(|ui| { - ui.label(label); - ui.text_edit_singleline(valor); - }); -} + let cb = checkbox(label, ativo).on_toggle(msg_toggle); -fn campo_letra_linha_opcional(ui: &mut Ui, label: &str, valor: &mut Option) { - ui.horizontal(|ui| { - let mut ativo = valor.is_some(); - if ui.checkbox(&mut ativo, label).changed() { - *valor = if ativo { Some(String::new()) } else { None }; - } - if let Some(v) = valor { - ui.text_edit_singleline(v); - } - }); + if ativo { + row![ + cb.width(Length::Fixed(200.0)), + text_input("ex: B3", &val_str) + .on_input(msg_valor) + .width(Length::Fixed(100.0)), + ] + .spacing(8) + .align_y(Alignment::Center) + .into() + } else { + row![cb].into() + } } /// Valida a configuração atual. Retorna (é_válido, lista_de_erros). @@ -340,7 +383,6 @@ fn validar_config(app: &App) -> (bool, Vec) { match &app.tipo_arquivo_atual { TipoArquivo::Csv => { let c = &app.layout_csv_atual; - // Verificar índices duplicados let mut indices: Vec<(String, usize)> = vec![ ("Numero".to_string(), c.indice_numero), ("Serie".to_string(), c.indice_serie), @@ -394,62 +436,3 @@ fn verificar_duplicados(indices: &[(String, usize)], erros: &mut Vec) { } } } - -fn executar_importacao(app: &mut App, ctx: &egui::Context) { - let caminho = match &app.caminho_arquivo { - Some(p) => p.clone(), - None => return, - }; - - let tipo = app.tipo_arquivo_atual.clone(); - let layout_csv = app.layout_csv_atual.clone(); - let layout_xlsx = app.layout_xlsx_atual.clone(); - - let (tx, rx) = std::sync::mpsc::channel(); - app.resultado_pendente = Some(rx); - app.estado = EstadoApp::Analisando; - ctx.request_repaint(); - - std::thread::spawn(move || { - // 1. Importar arquivo - let res_importacao = match tipo { - TipoArquivo::Csv => importar_csv(&caminho, &layout_csv).map_err(|e| e.to_string()), - TipoArquivo::Xlsx => importar_xlsx(&caminho, &layout_xlsx).map_err(|e| e.to_string()), - }; - - let res = match res_importacao { - Err(e) => ResultadoPendente::Erro(e), - Ok(importado) => { - if importado.notas.is_empty() { - ResultadoPendente::Vazio - } else { - let avisos = importado.avisos.clone(); - let notas = importado.notas; - - // 2. Pré-análise - let pre = pre_analisar(¬as); - let excessivos = series_com_intervalo_excessivo(&pre); - - if !excessivos.is_empty() { - ResultadoPendente::AguardandoConfirmacao { - pre, - series_excessivas: excessivos, - avisos, - notas, - } - } else { - // 3. Expandir faltantes - let resultado = expandir_analise(pre, ¬as); - ResultadoPendente::Concluido { - resultado, - avisos: Some(avisos), - notas: Some(notas), - } - } - } - } - }; - - let _ = tx.send(res); - }); -} diff --git a/src/ui/screens/import.rs b/src/ui/screens/import.rs index ceace11..301cfc3 100644 --- a/src/ui/screens/import.rs +++ b/src/ui/screens/import.rs @@ -1,328 +1,80 @@ -use crate::application::usecases::executar_analise::{ - expandir_analise, pre_analisar, series_com_intervalo_excessivo, -}; -use crate::application::usecases::importar_arquivo::{importar_xlsx, listar_abas_xlsx}; -use crate::domain::entities::layout::{Layout, TipoArquivo}; -use crate::ui::app::{App, EstadoApp, ResultadoPendente}; -use egui::{Context, Ui}; -use std::path::PathBuf; +use crate::ui::app::App; +use crate::ui::message::Message; +use iced::widget::{button, column, container, pick_list, row, text}; +use iced::{Alignment, Element, Length}; -/// Renderiza a tela de importação de arquivos. -pub fn renderizar(ui: &mut Ui, ctx: &Context, app: &mut App) { - ui.heading("Comparador de Notas — Importar Arquivo"); - ui.add_space(16.0); +/// Tela de importação de arquivos. +pub fn view(app: &App) -> Element<'_, Message> { + let titulo = text("Comparador de Notas — Importar Arquivo").size(22); - // --- Seleção de arquivo --- - ui.group(|ui| { - ui.label("Arquivo:"); - ui.horizontal(|ui| { - let nome = if app.nome_arquivo.is_empty() { - "Nenhum arquivo selecionado".to_string() - } else { - app.nome_arquivo.clone() - }; - ui.label(nome); + // Nome do arquivo selecionado + let nome = if app.nome_arquivo.is_empty() { + "Nenhum arquivo selecionado".to_string() + } else { + app.nome_arquivo.clone() + }; - if ui.button("📂 Selecionar arquivo...").clicked() { - if let Some(caminho) = rfd::FileDialog::new() - .add_filter("Planilhas", &["csv", "xlsx", "xls"]) - .pick_file() - { - on_arquivo_selecionado(app, ctx, caminho); - } - } - }); - }); + let secao_arquivo = row![ + text(nome).size(14).width(Length::Fill), + button("Selecionar arquivo...").on_press(Message::SelecionarArquivo), + ] + .spacing(8) + .align_y(Alignment::Center); - ui.add_space(8.0); - - // Coletar infos dos layouts antecipadamente para evitar borrow duplo + // Seletor de layout compatível com o tipo atual let tipo_atual = app.tipo_arquivo_atual.clone(); - let opcoes_layout: Vec<(i64, String)> = app + let opcoes_layout: Vec = app .layouts_salvos .iter() .filter(|l| l.tipo() == tipo_atual) - .filter_map(|l| l.id().map(|id| (id, l.nome().to_string()))) + .map(|l| l.nome().to_string()) .collect(); - let nome_layout_atual = app.nome_layout_atual.clone(); - ui.horizontal(|ui| { - ui.label("Layout:"); - egui::ComboBox::from_id_salt("combo_layouts_import") - .selected_text(if nome_layout_atual.is_empty() { - "— Selecionar layout —" - } else { - &nome_layout_atual - }) - .show_ui(ui, |ui| { - for (id, nome) in &opcoes_layout { - if ui - .selectable_label(nome_layout_atual == *nome, nome.as_str()) - .clicked() - { - app.nome_layout_atual = nome.clone(); - if let Some(layout) = - app.layouts_salvos.iter().find(|l| l.id() == Some(*id)) - { - let layout = layout.clone(); - aplicar_layout(app, &layout); - } - } - } - }); - - if ui.button("⚙ Gerenciar Layouts").clicked() { - app.estado = EstadoApp::GerenciandoLayouts; - } - }); - - ui.add_space(16.0); - - if app.caminho_arquivo.is_some() { - if ui.button("▶ Configurar Colunas").clicked() { - app.estado = EstadoApp::ConfigurandoColunas; - } - } -} - -/// Renderiza a tela de seleção de aba (XLSX). -pub fn renderizar_selecao_aba(ui: &mut Ui, ctx: &Context, app: &mut App) { - ui.heading("Selecionar Aba da Planilha"); - ui.add_space(16.0); - - let (abas, caminho) = match &app.estado { - EstadoApp::SelecionandoAba { abas, caminho } => (abas.clone(), caminho.clone()), - _ => return, + let nome_layout_sel: Option = if app.nome_layout_atual.is_empty() { + None + } else { + Some(app.nome_layout_atual.clone()) }; - ui.label(format!("Arquivo: {}", caminho.display())); - ui.add_space(8.0); - - // --- Seleção de preset --- - let opcoes_layout: Vec<(i64, String)> = app - .layouts_salvos - .iter() - .filter(|l| l.tipo() == TipoArquivo::Xlsx) - .filter_map(|l| l.id().map(|id| (id, l.nome().to_string()))) - .collect(); - let nome_layout_atual = app.nome_layout_atual.clone(); - - if !opcoes_layout.is_empty() { - ui.horizontal(|ui| { - ui.label("Layout:"); - egui::ComboBox::from_id_salt("combo_layouts_aba") - .selected_text(if nome_layout_atual.is_empty() { - "— Selecionar layout —" + let secao_layout = row![ + text("Layout:").size(14), + pick_list(opcoes_layout, nome_layout_sel, { + let layouts = app.layouts_salvos.clone(); + move |nome_selecionado: String| { + if let Some(id) = layouts + .iter() + .find(|l| l.nome() == nome_selecionado) + .and_then(|l| l.id()) + { + Message::LayoutSelecionado(id) } else { - &nome_layout_atual - }) - .show_ui(ui, |ui| { - for (id, nome) in &opcoes_layout { - if ui - .selectable_label(nome_layout_atual == *nome, nome.as_str()) - .clicked() - { - app.nome_layout_atual = nome.clone(); - if let Some(layout) = - app.layouts_salvos.iter().find(|l| l.id() == Some(*id)) - { - let layout = layout.clone(); - aplicar_layout(app, &layout); - } - } - } - }); - }); - ui.add_space(8.0); - } - - ui.label("Selecione a aba a processar:"); - - let aba_atual = app.layout_xlsx_atual.aba.clone(); - for aba in &abas { - if ui.selectable_label(aba_atual == *aba, aba).clicked() { - app.layout_xlsx_atual.aba = aba.clone(); - // Gerar pré-visualização da aba selecionada - app.preview_arquivo = - crate::infrastructure::xlsx_reader::preview_xlsx(&caminho, aba).ok(); - } - } - - // Pré-visualização da aba selecionada - if !app.layout_xlsx_atual.aba.is_empty() { - if let Some(preview) = &app.preview_arquivo { - ui.add_space(8.0); - crate::ui::screens::renderizar_tabela_preview(ui, preview); - } - } - - ui.add_space(12.0); - - if !app.layout_xlsx_atual.aba.is_empty() { - ui.horizontal(|ui| { - // Se há preset selecionado, oferecer processamento direto - let tem_preset = !app.nome_layout_atual.is_empty(); - if tem_preset { - let caminho_clone = caminho.clone(); - if ui.button("▶ Processar").clicked() { - app.nome_arquivo = caminho_clone - .file_name() - .map(|n| n.to_string_lossy().to_string()) - .unwrap_or_default(); - app.caminho_arquivo = Some(caminho_clone); - disparar_analise(app, ctx); + Message::NomeLayoutAlterado(nome_selecionado) } } + }) + .placeholder("— Selecionar layout —") + .width(Length::Fixed(260.0)), + button("⚙ Gerenciar Layouts").on_press(Message::IrParaLayouts), + ] + .spacing(8) + .align_y(Alignment::Center); - if ui.button("⚙ Configurar Colunas").clicked() { - app.nome_arquivo = caminho - .file_name() - .map(|n| n.to_string_lossy().to_string()) - .unwrap_or_default(); - app.caminho_arquivo = Some(caminho); - app.estado = EstadoApp::ConfigurandoColunas; - } - }); - } - - if ui.button("< Voltar").clicked() { - app.estado = EstadoApp::Importando; - } -} - -fn on_arquivo_selecionado(app: &mut App, ctx: &Context, caminho: PathBuf) { - let extensao = caminho - .extension() - .and_then(|e| e.to_str()) - .unwrap_or("") - .to_lowercase(); - - app.nome_arquivo = caminho - .file_name() - .map(|n| n.to_string_lossy().to_string()) - .unwrap_or_default(); - - match extensao.as_str() { - "csv" => { - app.tipo_arquivo_atual = TipoArquivo::Csv; - app.caminho_arquivo = Some(caminho.clone()); - app.notas_importadas.clear(); - // Gerar pré-visualização com o delimitador atual - app.preview_arquivo = crate::infrastructure::csv_reader::preview_csv( - &caminho, - app.layout_csv_atual.delimitador as u8, - &app.layout_csv_atual.encoding.clone(), - 5, - ) - .ok(); - } - "xlsx" | "xls" => { - app.tipo_arquivo_atual = TipoArquivo::Xlsx; - match listar_abas_xlsx(&caminho) { - Ok(info) => { - app.notas_importadas.clear(); - - // Verificar se há preset XLSX ativo com aba compatível - let preset_aba = if !app.nome_layout_atual.is_empty() { - let aba = app.layout_xlsx_atual.aba.clone(); - if !aba.is_empty() && info.abas.contains(&aba) { - Some(aba) - } else { - None - } - } else { - None - }; - - if let Some(aba) = preset_aba { - // Fluxo rápido: aba do preset existe → disparar análise direto - app.layout_xlsx_atual.aba = aba.clone(); - app.caminho_arquivo = Some(caminho.clone()); - app.abas_xlsx = info.abas; - app.preview_arquivo = - crate::infrastructure::xlsx_reader::preview_xlsx(&caminho, &aba).ok(); - disparar_analise(app, ctx); - } else { - // Fluxo normal: exibir tela de seleção de aba - app.abas_xlsx = info.abas.clone(); - app.estado = EstadoApp::SelecionandoAba { - abas: info.abas, - caminho: caminho.clone(), - }; - } - } - Err(e) => { - app.exibir_erro(format!("Erro ao ler abas do arquivo: {}", e)); - } - } - } - _ => { - app.exibir_erro("Formato de arquivo não suportado. Use CSV, XLSX ou XLS."); - } - } -} - -/// Dispara a análise assíncrona com o layout XLSX atual. -/// Usado tanto no fluxo rápido (preset com aba compatível) quanto no botão "Processar" da tela de aba. -fn disparar_analise(app: &mut App, ctx: &Context) { - let caminho = match &app.caminho_arquivo { - Some(p) => p.clone(), - None => return, + // Botão de avançar só aparece quando um arquivo foi selecionado + let botao_avancar: Element = if app.caminho_arquivo.is_some() { + button("▶ Configurar Colunas") + .on_press(Message::IrParaConfiguracaoColunas) + .into() + } else { + text("").into() }; - let layout_xlsx = app.layout_xlsx_atual.clone(); - let (tx, rx) = std::sync::mpsc::channel(); - app.resultado_pendente = Some(rx); - app.estado = EstadoApp::Analisando; - ctx.request_repaint(); + let content = column![titulo, secao_arquivo, secao_layout, botao_avancar] + .spacing(16) + .padding(24) + .width(Length::Fill); - std::thread::spawn(move || { - let res_importacao = importar_xlsx(&caminho, &layout_xlsx).map_err(|e| e.to_string()); - - let res = match res_importacao { - Err(e) => ResultadoPendente::Erro(e), - Ok(importado) => { - if importado.notas.is_empty() { - ResultadoPendente::Vazio - } else { - let avisos = importado.avisos.clone(); - let notas = importado.notas; - - let pre = pre_analisar(¬as); - let excessivos = series_com_intervalo_excessivo(&pre); - - if !excessivos.is_empty() { - ResultadoPendente::AguardandoConfirmacao { - pre, - series_excessivas: excessivos, - avisos, - notas, - } - } else { - let resultado = expandir_analise(pre, ¬as); - ResultadoPendente::Concluido { - resultado, - avisos: Some(avisos), - notas: Some(notas), - } - } - } - } - }; - - let _ = tx.send(res); - }); -} - -fn aplicar_layout(app: &mut App, layout: &Layout) { - match layout { - Layout::Csv { config, .. } => { - app.layout_csv_atual = config.clone(); - app.tipo_arquivo_atual = TipoArquivo::Csv; - } - Layout::Xlsx { config, .. } => { - app.layout_xlsx_atual = config.clone(); - app.tipo_arquivo_atual = TipoArquivo::Xlsx; - } - } + container(content) + .width(Length::Fill) + .height(Length::Fill) + .into() } diff --git a/src/ui/screens/layouts.rs b/src/ui/screens/layouts.rs index 69413a5..85582d0 100644 --- a/src/ui/screens/layouts.rs +++ b/src/ui/screens/layouts.rs @@ -1,264 +1,66 @@ -use crate::application::usecases::layouts::{ - exportar_layout_json, importar_layout_json, salvar_layout, -}; -use crate::domain::entities::layout::{Layout, LayoutJson, TipoArquivo}; -use crate::domain::errors::ErroLayout; -use crate::ui::app::{AcaoModal, App, EstadoApp}; -use egui::{Context, Ui}; +use crate::domain::entities::layout::{Layout, TipoArquivo}; +use crate::ui::app::App; +use crate::ui::message::Message; +use iced::widget::{button, column, container, horizontal_space, row, scrollable, text}; +use iced::{Alignment, Element, Length}; -/// Renderiza a tela de gerenciamento de layouts. -pub fn renderizar(ui: &mut Ui, ctx: &Context, app: &mut App) { - ui.heading("Gerenciar Layouts"); - ui.add_space(8.0); +/// Tela de gerenciamento de layouts. +pub fn view(app: &App) -> Element<'_, Message> { + let titulo = text("Gerenciar Layouts").size(22); - ui.horizontal(|ui| { - if ui.button("< Voltar").clicked() { - app.estado = EstadoApp::Importando; - } - }); + let botao_voltar = row![button("< Voltar").on_press(Message::IrParaImportacao)].spacing(8); - ui.add_space(12.0); - ui.separator(); + let secao_csv = view_secao_layouts("Layouts CSV", &app.layouts_salvos, TipoArquivo::Csv); + let secao_xlsx = view_secao_layouts("Layouts XLSX", &app.layouts_salvos, TipoArquivo::Xlsx); - // Salvar layout atual - ui.add_space(8.0); - ui.group(|ui| { - ui.label("Salvar Layout Atual"); - ui.horizontal(|ui| { - ui.label("Nome:"); - ui.text_edit_singleline(&mut app.nome_layout_atual); - if ui.button("💾 Salvar").clicked() { - salvar_layout_atual(app); - } - }); - }); + // Importar de JSON + let importar_row = row![ + text("Importar layout de arquivo JSON:").size(14), + button("📥 Importar JSON").on_press(Message::ImportarLayoutJson), + ] + .spacing(8) + .align_y(Alignment::Center); - ui.add_space(12.0); - ui.separator(); - ui.add_space(8.0); + let content = column![titulo, botao_voltar, secao_csv, secao_xlsx, importar_row,] + .spacing(16) + .padding(16) + .width(Length::Fill); - // Layouts CSV - let layouts_csv: Vec<_> = app - .layouts_salvos - .iter() - .filter(|l| l.tipo() == TipoArquivo::Csv) - .cloned() - .collect(); - - let layouts_xlsx: Vec<_> = app - .layouts_salvos - .iter() - .filter(|l| l.tipo() == TipoArquivo::Xlsx) - .cloned() - .collect(); - - egui::ScrollArea::vertical().show(ui, |ui| { - renderizar_secao_layouts(ui, ctx, app, "Layouts CSV", &layouts_csv); - ui.add_space(12.0); - renderizar_secao_layouts(ui, ctx, app, "Layouts XLSX", &layouts_xlsx); - - ui.add_space(16.0); - ui.separator(); - ui.add_space(8.0); - - // Importar de JSON - ui.horizontal(|ui| { - ui.label("Importar layout de arquivo JSON:"); - if ui.button("📥 Importar JSON").clicked() { - if let Some(caminho) = rfd::FileDialog::new() - .add_filter("JSON", &["json"]) - .pick_file() - { - match std::fs::read_to_string(&caminho) { - Ok(conteudo) => importar_json(app, &conteudo), - Err(e) => { - app.exibir_erro(format!("Erro ao ler arquivo JSON: {}", e)); - } - } - } - } - }); - }); + container(scrollable(content)) + .width(Length::Fill) + .height(Length::Fill) + .into() } -fn renderizar_secao_layouts( - ui: &mut Ui, - _ctx: &Context, - app: &mut App, - titulo: &str, - layouts: &[Layout], -) { - ui.label(egui::RichText::new(titulo).strong()); - ui.add_space(4.0); +fn view_secao_layouts<'a>( + titulo: &'a str, + layouts: &'a [Layout], + tipo: TipoArquivo, +) -> Element<'a, Message> { + let mut col = column![text(titulo).size(16)].spacing(4); - if layouts.is_empty() { - ui.label("(nenhum layout salvo)"); - return; + let filtrados: Vec<&Layout> = layouts.iter().filter(|l| l.tipo() == tipo).collect(); + + if filtrados.is_empty() { + col = col.push(text("(nenhum layout salvo)").size(13)); + return col.into(); } - for layout in layouts { - ui.horizontal(|ui| { - ui.label(layout.nome()); - ui.with_layout(egui::Layout::right_to_left(egui::Align::Center), |ui| { - // Excluir - if let Some(id) = layout.id() { - if ui.button("🗑 Excluir").clicked() { - app.exibir_confirmacao( - "Confirmar exclusão", - format!("Deseja excluir o layout '{}'?", layout.nome()), - AcaoModal::ConfirmarExclusaoLayout(id), - ); - } + for layout in filtrados { + if let Some(id) = layout.id() { + let linha = row![ + text(layout.nome()).size(14).width(Length::Fill), + horizontal_space(), + button("📂 Carregar").on_press(Message::LayoutSelecionado(id)), + button("📤 Exportar JSON").on_press(Message::ExportarLayoutJson(id)), + button("🗑 Excluir").on_press(Message::ExcluirLayout(id)), + ] + .spacing(8) + .align_y(Alignment::Center); - // Exportar - if ui.button("📤 Exportar JSON").clicked() { - match exportar_layout_json(layout) { - Ok((conteudo, nome_sugerido)) => { - if let Some(caminho) = rfd::FileDialog::new() - .set_file_name(&nome_sugerido) - .add_filter("JSON", &["json"]) - .save_file() - { - if let Err(e) = std::fs::write(&caminho, &conteudo) { - app.exibir_erro(format!("Erro ao salvar JSON: {}", e)); - } - } - } - Err(e) => { - app.exibir_erro(format!("Erro ao exportar layout: {}", e)); - } - } - } - - // Carregar - if ui.button("📂 Carregar").clicked() { - aplicar_layout(app, layout); - app.nome_layout_atual = layout.nome().to_string(); - app.estado = EstadoApp::ConfigurandoColunas; - } - } - }); - }); - } -} - -fn salvar_layout_atual(app: &mut App) { - if app.nome_layout_atual.trim().is_empty() { - app.exibir_aviso("Nome inválido", "Informe um nome para o layout."); - return; - } - - let layout = match app.tipo_arquivo_atual.clone() { - TipoArquivo::Csv => Layout::Csv { - id: None, - nome: app.nome_layout_atual.trim().to_string(), - config: app.layout_csv_atual.clone(), - }, - TipoArquivo::Xlsx => Layout::Xlsx { - id: None, - nome: app.nome_layout_atual.trim().to_string(), - config: app.layout_xlsx_atual.clone(), - }, - }; - - if let Some(conn) = &app.conn { - match salvar_layout(conn, &layout) { - Ok(_) => { - app.recarregar_layouts(); - app.exibir_aviso("Sucesso", "Layout salvo com sucesso."); - } - Err(ErroLayout::NomeConflitante(nome)) => { - let id_existente = app - .layouts_salvos - .iter() - .find(|l| l.nome() == nome) - .and_then(|l| l.id()); - if let Some(id) = id_existente { - let layout_com_id = match app.tipo_arquivo_atual.clone() { - TipoArquivo::Csv => Layout::Csv { - id: Some(id), - nome: nome.clone(), - config: app.layout_csv_atual.clone(), - }, - TipoArquivo::Xlsx => Layout::Xlsx { - id: Some(id), - nome: nome.clone(), - config: app.layout_xlsx_atual.clone(), - }, - }; - app.exibir_confirmacao( - "Conflito de nome", - format!( - "Já existe um layout com o nome '{}'. Deseja sobrescrever?", - nome - ), - AcaoModal::SobrescreverLayout(layout_com_id), - ); - } - } - Err(e) => { - app.exibir_erro(format!("Erro ao salvar layout: {}", e)); - } - } - } -} - -fn importar_json(app: &mut App, conteudo: &str) { - if let Some(conn) = &app.conn { - match importar_layout_json(conn, conteudo, false, None) { - Ok(_) => { - app.recarregar_layouts(); - app.exibir_aviso("Sucesso", "Layout importado com sucesso."); - } - Err(ErroLayout::NomeConflitante(nome)) => { - // Recriar o layout parseado para passá-lo no modal de confirmação. - // O JSON já foi validado pela chamada acima, então o parse aqui não falha. - let parsed = serde_json::from_str::(conteudo) - .ok() - .and_then(|json_repr| Layout::try_from(json_repr).ok()); - - let id_existente = app - .layouts_salvos - .iter() - .find(|l| l.nome() == nome) - .and_then(|l| l.id()); - - match (parsed, id_existente) { - (Some(mut layout), Some(id)) => { - match &mut layout { - Layout::Csv { id: i, .. } => *i = Some(id), - Layout::Xlsx { id: i, .. } => *i = Some(id), - } - app.exibir_confirmacao( - "Conflito de nome", - format!( - "Já existe um layout com o nome '{}'. Deseja sobrescrever?", - nome - ), - AcaoModal::SobrescreverLayout(layout), - ); - } - _ => { - app.exibir_erro(format!("Conflito de nome: layout '{}' já existe.", nome)); - } - } - } - Err(e) => { - app.exibir_erro(format!("Erro ao importar layout: {}", e)); - } - } - } -} - -fn aplicar_layout(app: &mut App, layout: &Layout) { - match layout { - Layout::Csv { config, .. } => { - app.layout_csv_atual = config.clone(); - app.tipo_arquivo_atual = TipoArquivo::Csv; - } - Layout::Xlsx { config, .. } => { - app.layout_xlsx_atual = config.clone(); - app.tipo_arquivo_atual = TipoArquivo::Xlsx; + col = col.push(linha); } } + + col.into() } diff --git a/src/ui/screens/mod.rs b/src/ui/screens/mod.rs index 6647f5b..7c42fb6 100644 --- a/src/ui/screens/mod.rs +++ b/src/ui/screens/mod.rs @@ -2,9 +2,10 @@ pub mod configuracao_colunas; pub mod import; pub mod layouts; pub mod resultado; +pub mod selecionar_aba; /// Converte um índice de coluna base-0 para a notação de letras do Excel (A, B, ..., Z, AA, ...). -fn indice_para_letra(mut idx: usize) -> String { +pub fn indice_para_letra(mut idx: usize) -> String { let mut resultado = String::new(); loop { resultado.insert(0, (b'A' + (idx % 26) as u8) as char); @@ -15,55 +16,3 @@ fn indice_para_letra(mut idx: usize) -> String { } resultado } - -/// Renderiza uma tabela simples de pré-visualização do arquivo. -/// Exibe uma linha de cabeçalho com letras no estilo Excel (A, B, C, ...) -/// seguida pelas linhas de dados. -pub fn renderizar_tabela_preview(ui: &mut egui::Ui, linhas: &[Vec]) { - let num_colunas = linhas.iter().map(|l| l.len()).max().unwrap_or(0); - if num_colunas == 0 { - return; - } - - ui.label( - egui::RichText::new(format!("Pré-visualização ({} linha(s))", linhas.len())) - .small() - .weak(), - ); - ui.add_space(2.0); - - egui::ScrollArea::horizontal() - .id_salt("scroll_preview") - .max_height(160.0) - .show(ui, |ui| { - egui::Grid::new("tabela_preview") - .striped(true) - .spacing([8.0, 2.0]) - .show(ui, |ui| { - // Linha de cabeçalho: letras A, B, C, ... com índice base-0 entre parênteses - for i in 0..num_colunas { - ui.label( - egui::RichText::new(format!("{} ({})", indice_para_letra(i), i)) - .strong() - .monospace(), - ); - } - ui.end_row(); - - // Linhas de dados - for linha in linhas { - for col in 0..num_colunas { - let celula = linha.get(col).map(|s| s.as_str()).unwrap_or(""); - let texto = if celula.chars().count() > 30 { - let truncado: String = celula.chars().take(30).collect(); - format!("{}...", truncado) - } else { - celula.to_string() - }; - ui.label(egui::RichText::new(texto).monospace().small()); - } - ui.end_row(); - } - }); - }); -} diff --git a/src/ui/screens/resultado.rs b/src/ui/screens/resultado.rs index adab075..33bd619 100644 --- a/src/ui/screens/resultado.rs +++ b/src/ui/screens/resultado.rs @@ -1,127 +1,76 @@ -use crate::application::usecases::exportar_pdf::exportar_pdf; use crate::domain::{ entities::{chave_serie::ChaveSerie, resultado_analise::ResultadoAnalise}, services::{detector_sequencia::agrupar_contiguos, parser_monetario::formatar_valor_br}, }; -use crate::infrastructure::pdf_generator::GenpdfGenerator; -use crate::ui::app::{AcaoModal, App, EstadoApp}; -use egui::{Context, Ui}; +use crate::ui::app::App; +use crate::ui::message::Message; +use iced::widget::{button, column, container, row, scrollable, text}; +use iced::{Alignment, Element, Length}; const OPCOES_PAGINA: &[usize] = &[50, 100, 200, 1000]; -/// Renderiza a tela de resultados. -pub fn renderizar(ui: &mut Ui, ctx: &Context, app: &mut App) { - // Extrair resultado do estado (sem mover) - let resultado = match &app.estado { - EstadoApp::ExibindoResultado(r) => r.clone(), - _ => return, - }; +/// Tela de resultados da análise. +pub fn view<'a>(app: &'a App, resultado: &'a ResultadoAnalise) -> Element<'a, Message> { + let titulo = text("Resultado da Análise").size(22); - ui.heading("Resultado da Análise"); - ui.add_space(8.0); - - ui.horizontal(|ui| { - if ui.button("< Nova Análise").clicked() { - app.exibir_confirmacao( - "Nova Análise", - "Deseja iniciar uma nova análise? O resultado atual será descartado.", - AcaoModal::ConfirmarNovaAnalise, - ); - } - - if ui.button("⚙ Reconfigurar Colunas").clicked() { - app.estado = EstadoApp::ConfigurandoColunas; - return; - } - - let pode_reanalisar = app.caminho_arquivo.is_some(); - if ui - .add_enabled(pode_reanalisar, egui::Button::new("🔄 Reanalisar Arquivo")) - .on_hover_text("Reimporta o arquivo do disco com o layout atual e reanalisa") - .clicked() - { - app.reimportar_e_analisar(ctx); - } - - if ui.button("📄 Exportar PDF").clicked() { - exportar_para_pdf(app, &resultado); - } - }); - - ui.add_space(8.0); + // Botões de ação superiores + let botoes_topo = row![ + button("< Nova Análise").on_press(Message::NovaAnalise), // abre modal de confirmação + button("⚙ Reconfigurar Colunas").on_press(Message::IrParaConfiguracaoColunas), + button("🔄 Reanalisar Arquivo").on_press_maybe( + app.caminho_arquivo + .as_ref() + .map(|_| Message::ReanalisarArquivo) + ), + button("📄 Exportar PDF").on_press(Message::ExportarPdf), + ] + .spacing(8); // Controle de itens por página - ui.horizontal(|ui| { - ui.label("Itens por página:"); - for &opcao in OPCOES_PAGINA { - if ui - .selectable_label(app.itens_por_pagina == opcao, opcao.to_string()) - .clicked() - { - app.itens_por_pagina = opcao; - app.pagina_faltantes = 0; - app.pagina_duplicatas = 0; - } - } - }); + let opcoes_por_pagina = row(OPCOES_PAGINA + .iter() + .map(|&n| { + button(text(n.to_string()).size(13)) + .on_press(Message::ItensPorPaginaAlterado(n)) + .into() + }) + .collect::>()) + .spacing(4); - ui.add_space(12.0); - ui.separator(); + let controle_pagina = row![text("Itens por página:").size(13), opcoes_por_pagina,] + .spacing(8) + .align_y(Alignment::Center); - egui::ScrollArea::vertical().show(ui, |ui| { - // Faltantes - renderizar_faltantes(ui, app, &resultado); + // Seções principais + let secao_faltantes = view_faltantes(app, resultado); + let secao_duplicatas = view_duplicatas(app, resultado); + let secao_totais = view_totais(resultado); - ui.add_space(12.0); - ui.separator(); + let conteudo = column![ + titulo, + botoes_topo, + controle_pagina, + secao_faltantes, + secao_duplicatas, + secao_totais, + ] + .spacing(16) + .padding(16) + .width(Length::Fill); - // Duplicatas - renderizar_duplicatas(ui, app, &resultado); - - ui.add_space(12.0); - ui.separator(); - - // Totais - renderizar_totais(ui, &resultado); - }); + container(scrollable(conteudo)) + .width(Length::Fill) + .height(Length::Fill) + .into() } -fn renderizar_totais(ui: &mut Ui, resultado: &ResultadoAnalise) { - ui.label(egui::RichText::new("Totais").heading().strong()); - ui.add_space(4.0); +fn view_faltantes<'a>(app: &'a App, resultado: &'a ResultadoAnalise) -> Element<'a, Message> { + let total = resultado.total_faltantes(); + let mut col = column![text(format!("Notas Faltantes ({} total)", total)).size(18),].spacing(8); - ui.label(format!( - "Total Geral: R$ {}", - formatar_valor_br(&resultado.soma_total) - )); - - let mut chaves: Vec<&ChaveSerie> = resultado.soma_por_serie.keys().collect(); - chaves.sort(); - - for chave in chaves { - let soma = &resultado.soma_por_serie[chave]; - let total_notas = resultado.total_por_serie.get(chave).copied().unwrap_or(0); - ui.label(format!( - " Série {}: {} nota(s) — R$ {}", - chave.label(), - total_notas, - formatar_valor_br(soma) - )); - } -} - -fn renderizar_faltantes(ui: &mut Ui, app: &mut App, resultado: &ResultadoAnalise) { - let total_faltantes = resultado.total_faltantes(); - ui.label( - egui::RichText::new(format!("Notas Faltantes ({} total)", total_faltantes)) - .heading() - .strong(), - ); - ui.add_space(4.0); - - if total_faltantes == 0 { - ui.label("✔ Nenhuma nota faltante."); - return; + if total == 0 { + col = col.push(text("✔ Nenhuma nota faltante.").size(14)); + return col.into(); } let mut chaves: Vec<&ChaveSerie> = resultado.faltantes_por_serie.keys().collect(); @@ -133,82 +82,63 @@ fn renderizar_faltantes(ui: &mut Ui, app: &mut App, resultado: &ResultadoAnalise continue; } - // Estatística de completude por série let total_notas = resultado.total_por_serie.get(chave).copied().unwrap_or(0); let total_esperado = total_notas + faltantes.len(); let percentual = total_notas as f64 / total_esperado as f64 * 100.0; - ui.horizontal(|ui| { - ui.label(format!( + let cabecalho_serie = row![ + text(format!( "Série {} — {} faltante(s) — {}/{} notas ({:.1}% completo):", chave.label(), faltantes.len(), total_notas, total_esperado, percentual, - )); - if ui - .button("📋 Copiar") - .on_hover_text("Copiar todos os números faltantes") - .clicked() - { - let texto = faltantes - .iter() - .map(|n| n.to_string()) - .collect::>() - .join(", "); - ui.ctx().copy_text(texto); - } - }); + )) + .size(14) + .width(Length::Fill), + button("📋 Copiar").on_press(Message::CopiarFaltantes(chave.clone())), + ] + .spacing(8) + .align_y(Alignment::Center); - // Paginação (por faltante individual) + col = col.push(cabecalho_serie); + + // Paginação let total_paginas = (faltantes.len() + app.itens_por_pagina - 1) / app.itens_por_pagina; - if app.pagina_faltantes >= total_paginas { - app.pagina_faltantes = 0; - } - - let inicio = app.pagina_faltantes * app.itens_por_pagina; + let pagina = app.pagina_faltantes.min(total_paginas.saturating_sub(1)); + let inicio = pagina * app.itens_por_pagina; let fim = (inicio + app.itens_por_pagina).min(faltantes.len()); - // Exibir grupos contíguos da página atual for (a, b) in agrupar_contiguos(&faltantes[inicio..fim]) { if a == b { - ui.label(format!(" • {}", a)); + col = col.push(text(format!(" • {}", a)).size(13)); } else { - ui.label(format!(" • {}–{} ({} notas)", a, b, b - a + 1)); + col = col.push(text(format!(" • {}–{} ({} notas)", a, b, b - a + 1)).size(13)); } } if total_paginas > 1 { - ui.horizontal(|ui| { - if ui.button("◀").clicked() && app.pagina_faltantes > 0 { - app.pagina_faltantes -= 1; - } - ui.label(format!( - "Página {} / {}", - app.pagina_faltantes + 1, - total_paginas - )); - if ui.button("▶").clicked() && app.pagina_faltantes + 1 < total_paginas { - app.pagina_faltantes += 1; - } - }); + col = col.push(crate::ui::components::paginacao::controles_paginacao( + pagina, + total_paginas, + Message::PaginaFaltantesAlterada(pagina.saturating_sub(1)), + Message::PaginaFaltantesAlterada(pagina + 1), + )); } } + + col.into() } -fn renderizar_duplicatas(ui: &mut Ui, app: &mut App, resultado: &ResultadoAnalise) { - let total_dup = resultado.total_duplicatas(); - ui.label( - egui::RichText::new(format!("Notas Duplicadas ({} grupo(s))", total_dup)) - .heading() - .strong(), - ); - ui.add_space(4.0); +fn view_duplicatas<'a>(app: &'a App, resultado: &'a ResultadoAnalise) -> Element<'a, Message> { + let total = resultado.total_duplicatas(); + let mut col = + column![text(format!("Notas Duplicadas ({} grupo(s))", total)).size(18),].spacing(8); - if total_dup == 0 { - ui.label("✔ Nenhuma nota duplicada."); - return; + if total == 0 { + col = col.push(text("✔ Nenhuma nota duplicada.").size(14)); + return col.into(); } let mut chaves: Vec<&ChaveSerie> = resultado.duplicadas_por_serie.keys().collect(); @@ -220,90 +150,78 @@ fn renderizar_duplicatas(ui: &mut Ui, app: &mut App, resultado: &ResultadoAnalis continue; } - ui.horizontal(|ui| { - ui.label(format!( + let cabecalho_serie = row![ + text(format!( "Série {} — {} grupo(s) duplicado(s):", chave.label(), duplicatas.len() - )); - if ui - .button("📋 Copiar") - .on_hover_text("Copiar números duplicados") - .clicked() - { - let texto = duplicatas - .iter() - .map(|(n, c)| format!("{} ({}x)", n, c)) - .collect::>() - .join(", "); - ui.ctx().copy_text(texto); - } - }); + )) + .size(14) + .width(Length::Fill), + button("📋 Copiar").on_press(Message::CopiarDuplicatas(chave.clone())), + ] + .spacing(8) + .align_y(Alignment::Center); + + col = col.push(cabecalho_serie); let total_paginas = (duplicatas.len() + app.itens_por_pagina - 1) / app.itens_por_pagina; - if app.pagina_duplicatas >= total_paginas { - app.pagina_duplicatas = 0; - } - - let inicio = app.pagina_duplicatas * app.itens_por_pagina; + let pagina = app.pagina_duplicatas.min(total_paginas.saturating_sub(1)); + let inicio = pagina * app.itens_por_pagina; let fim = (inicio + app.itens_por_pagina).min(duplicatas.len()); for (numero, count) in &duplicatas[inicio..fim] { - ui.label(format!( - " • NF {} / Série {} — {} ocorrências", - numero, - chave.label(), - count - )); + col = col.push( + text(format!( + " • NF {} / Série {} — {} ocorrências", + numero, + chave.label(), + count + )) + .size(13), + ); } if total_paginas > 1 { - ui.horizontal(|ui| { - if ui.button("◀").clicked() && app.pagina_duplicatas > 0 { - app.pagina_duplicatas -= 1; - } - ui.label(format!( - "Página {} / {}", - app.pagina_duplicatas + 1, - total_paginas - )); - if ui.button("▶").clicked() && app.pagina_duplicatas + 1 < total_paginas { - app.pagina_duplicatas += 1; - } - }); + col = col.push(crate::ui::components::paginacao::controles_paginacao( + pagina, + total_paginas, + Message::PaginaDuplicatasAlterada(pagina.saturating_sub(1)), + Message::PaginaDuplicatasAlterada(pagina + 1), + )); } } + + col.into() } -fn exportar_para_pdf(app: &mut App, resultado: &ResultadoAnalise) { - if let Some(caminho) = rfd::FileDialog::new() - .set_file_name("relatorio.pdf") - .add_filter("PDF", &["pdf"]) - .save_file() - { - let gerador = GenpdfGenerator; - let nome_layout = if app.nome_layout_atual.is_empty() { - None - } else { - Some(app.nome_layout_atual.as_str()) - }; +fn view_totais(resultado: &ResultadoAnalise) -> Element<'_, Message> { + let mut col = column![text("Totais").size(18)].spacing(4); - match exportar_pdf( - &gerador, - resultado, - &app.nome_arquivo, - nome_layout, - &caminho, - ) { - Ok(_) => { - app.exibir_aviso( - "Sucesso", - format!("PDF exportado para: {}", caminho.display()), - ); - } - Err(e) => { - app.exibir_erro(format!("Erro ao exportar PDF: {}", e)); - } - } + col = col.push( + text(format!( + "Total Geral: R$ {}", + formatar_valor_br(&resultado.soma_total) + )) + .size(14), + ); + + let mut chaves: Vec<&ChaveSerie> = resultado.soma_por_serie.keys().collect(); + chaves.sort(); + + for chave in chaves { + let soma = &resultado.soma_por_serie[chave]; + let total_notas = resultado.total_por_serie.get(chave).copied().unwrap_or(0); + col = col.push( + text(format!( + " Série {}: {} nota(s) — R$ {}", + chave.label(), + total_notas, + formatar_valor_br(soma) + )) + .size(13), + ); } + + col.into() } diff --git a/src/ui/screens/selecionar_aba.rs b/src/ui/screens/selecionar_aba.rs new file mode 100644 index 0000000..9e8a49e --- /dev/null +++ b/src/ui/screens/selecionar_aba.rs @@ -0,0 +1,79 @@ +use crate::ui::app::App; +use crate::ui::message::Message; +use iced::widget::{button, column, container, row, scrollable, text}; +use iced::{Element, Length}; +use std::path::PathBuf; + +/// Tela de seleção de aba de arquivo XLSX. +pub fn view<'a>(app: &'a App, abas: &'a [String], caminho: &'a PathBuf) -> Element<'a, Message> { + let titulo = text("Selecionar Aba da Planilha").size(22); + + let arquivo_label = text(format!("Arquivo: {}", caminho.display())).size(13); + + // Lista de abas como botões seleccionáveis + let aba_atual = &app.layout_xlsx_atual.aba; + let lista_abas = column( + abas.iter() + .map(|aba| { + let selecionada = aba == aba_atual; + let btn = if selecionada { + button(text(aba).size(14)) + .on_press(Message::AbaSelecionada(aba.clone())) + .width(Length::Fill) + } else { + button(text(aba).size(14)) + .on_press(Message::AbaSelecionada(aba.clone())) + .width(Length::Fill) + }; + btn.into() + }) + .collect::>(), + ) + .spacing(4) + .width(Length::Fill); + + // Pré-visualização da aba selecionada + let preview_section: Element = if !aba_atual.is_empty() { + if let Some(linhas) = &app.preview_arquivo { + crate::ui::components::tabela_preview::tabela_preview(linhas) + } else { + text("(sem pré-visualização)").size(12).into() + } + } else { + text("Selecione uma aba para pré-visualizar.") + .size(12) + .into() + }; + + // Botões de ação + let tem_preset = !app.nome_layout_atual.is_empty(); + let aba_selecionada = !aba_atual.is_empty(); + + let mut botoes = row![button("< Voltar").on_press(Message::Voltar)].spacing(8); + + if aba_selecionada && tem_preset { + botoes = botoes.push(button("▶ Processar").on_press(Message::ExecutarImportacao)); + } + + if aba_selecionada { + botoes = botoes + .push(button("⚙ Configurar Colunas").on_press(Message::IrParaConfiguracaoColunas)); + } + + let content = column![ + titulo, + arquivo_label, + text("Selecione a aba a processar:").size(14), + scrollable(lista_abas).height(Length::Fixed(200.0)), + preview_section, + botoes, + ] + .spacing(12) + .padding(16) + .width(Length::Fill); + + container(content) + .width(Length::Fill) + .height(Length::Fill) + .into() +} diff --git a/ui-ideia/a1134fe1-ae81-425d-b386-7dd99f51486b.png b/ui-ideia/a1134fe1-ae81-425d-b386-7dd99f51486b.png new file mode 100644 index 0000000..ea0b5d4 Binary files /dev/null and b/ui-ideia/a1134fe1-ae81-425d-b386-7dd99f51486b.png differ diff --git a/ui-ideia/design_tokens.json b/ui-ideia/design_tokens.json new file mode 100644 index 0000000..87b5ada --- /dev/null +++ b/ui-ideia/design_tokens.json @@ -0,0 +1,109 @@ +{ + "color": { + "primary": { + "base": "#3B82F6", + "hover": "#2563EB", + "active": "#1D4ED8" + }, + + "semantic": { + "success": "#22C55E", + "warning": "#F59E0B", + "error": "#EF4444", + "info": "#0EA5E9" + }, + + "dark": { + "background": "#0F172A", + "surface": "#1E293B", + "surfaceSecondary": "#334155", + + "text": { + "primary": "#F1F5F9", + "secondary": "#94A3B8", + "muted": "#64748B" + }, + + "border": "#334155", + + "interaction": { + "hover": "#3B82F622", + "selection": "#3B82F633", + "focus": "#3B82F6" + } + }, + + "light": { + "background": "#F8FAFC", + "surface": "#FFFFFF", + "surfaceSecondary": "#F1F5F9", + + "text": { + "primary": "#0F172A", + "secondary": "#64748B", + "muted": "#94A3B8" + }, + + "border": "#E2E8F0", + + "interaction": { + "hover": "#3B82F611", + "selection": "#3B82F622", + "focus": "#3B82F6" + } + } + }, + + "table": { + "row": { + "hover": "#3B82F611", + "selected": "#3B82F622" + }, + + "status": { + "ok": "#22C55E", + "missing": "#F59E0B", + "duplicate": "#EF4444", + "invalid": "#EF4444" + } + }, + + "spacing": { + "xs": 4, + "sm": 8, + "md": 12, + "lg": 16, + "xl": 24, + "xxl": 32 + }, + + "radius": { + "sm": 4, + "md": 6, + "lg": 8 + }, + + "font": { + "family": "Inter, system-ui, sans-serif", + + "size": { + "xs": 11, + "sm": 12, + "md": 14, + "lg": 16, + "xl": 20 + }, + + "weight": { + "normal": 400, + "medium": 500, + "bold": 600 + } + }, + + "shadow": { + "sm": "0 1px 2px rgba(0,0,0,0.05)", + "md": "0 4px 8px rgba(0,0,0,0.08)", + "lg": "0 10px 20px rgba(0,0,0,0.12)" + } +} diff --git a/ui-ideia/mockup.html b/ui-ideia/mockup.html new file mode 100644 index 0000000..b0083cc --- /dev/null +++ b/ui-ideia/mockup.html @@ -0,0 +1,287 @@ +```html + + + + + Comparador de Notas - Mockup + + + + + +
+
+
Comparador de Notas
+ +
+ + + +
+
+ +
+
+ 48 Notas Faltantes +
+
+ 6 Notas Duplicadas +
+
R$ 125.600,00 Total
+
+ +
+
+ Série 1 – NFE — 48 / 50 notas — 96% completo +
+
+
+
+ +
+ Série 2 – NFCE — 20 / 25 notas — 80% completo +
+
+
+
+ +
+ Série 3 – NFE — 12 / 20 notas — 60% completo +
+
+
+
+
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
NúmeroSérieTipoValorStatus
10051NFER$ 2.500,00Duplicada
10061NFER$ 3.200,00OK
10101NFER$ 4.000,00 + Falta: 1010–1050 +
10511NFER$ 2.800,00OK
10752NFCER$ 1.200,00Duplicada
+ + +
+ + +```