diff --git a/.gitignore b/.gitignore index b7a788c..0e76d10 100644 --- a/.gitignore +++ b/.gitignore @@ -1,7 +1,7 @@ .cargo/ +Cargo.lock *.pdb **/*.rs.bk -tmp/ debug/ target/ vendor/ diff --git a/Cargo.lock b/Cargo.lock deleted file mode 100644 index c9a81f3..0000000 --- a/Cargo.lock +++ /dev/null @@ -1,7880 +0,0 @@ -# This file is automatically @generated by Cargo. -# It is not intended for manual editing. -version = 4 - -[[package]] -name = "ab_glyph" -version = "0.2.32" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "01c0457472c38ea5bd1c3b5ada5e368271cb550be7a4ca4a0b4634e9913f6cc2" -dependencies = [ - "ab_glyph_rasterizer", - "owned_ttf_parser", -] - -[[package]] -name = "ab_glyph_rasterizer" -version = "0.1.10" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "366ffbaa4442f4684d91e2cd7c5ea7c4ed8add41959a31447066e279e432b618" - -[[package]] -name = "accesskit" -version = "0.16.0" -source = "git+https://github.com/wash2/accesskit?tag=iced-xdg-surface-0.13-rc#c46afc041b1968a5af0186fa6aba3ea9cf24c8c3" - -[[package]] -name = "accesskit_atspi_common" -version = "0.9.0" -source = "git+https://github.com/wash2/accesskit?tag=iced-xdg-surface-0.13-rc#c46afc041b1968a5af0186fa6aba3ea9cf24c8c3" -dependencies = [ - "accesskit", - "accesskit_consumer", - "atspi-common", - "serde", - "thiserror 1.0.69", - "zvariant 3.15.2", -] - -[[package]] -name = "accesskit_consumer" -version = "0.24.0" -source = "git+https://github.com/wash2/accesskit?tag=iced-xdg-surface-0.13-rc#c46afc041b1968a5af0186fa6aba3ea9cf24c8c3" -dependencies = [ - "accesskit", - "immutable-chunkmap", -] - -[[package]] -name = "accesskit_macos" -version = "0.17.0" -source = "git+https://github.com/wash2/accesskit?tag=iced-xdg-surface-0.13-rc#c46afc041b1968a5af0186fa6aba3ea9cf24c8c3" -dependencies = [ - "accesskit", - "accesskit_consumer", - "objc2 0.5.2", - "objc2-app-kit 0.2.2", - "objc2-foundation 0.2.2", - "once_cell", -] - -[[package]] -name = "accesskit_unix" -version = "0.12.0" -source = "git+https://github.com/wash2/accesskit?tag=iced-xdg-surface-0.13-rc#c46afc041b1968a5af0186fa6aba3ea9cf24c8c3" -dependencies = [ - "accesskit", - "accesskit_atspi_common", - "atspi", - "futures-lite 1.13.0", - "serde", - "tokio", - "tokio-stream", - "zbus 3.15.2", -] - -[[package]] -name = "accesskit_windows" -version = "0.22.0" -source = "git+https://github.com/wash2/accesskit?tag=iced-xdg-surface-0.13-rc#c46afc041b1968a5af0186fa6aba3ea9cf24c8c3" -dependencies = [ - "accesskit", - "accesskit_consumer", - "paste", - "static_assertions", - "windows 0.54.0", -] - -[[package]] -name = "accesskit_winit" -version = "0.22.0" -source = "git+https://github.com/wash2/accesskit?tag=iced-xdg-surface-0.13-rc#c46afc041b1968a5af0186fa6aba3ea9cf24c8c3" -dependencies = [ - "accesskit", - "accesskit_macos", - "accesskit_unix", - "accesskit_windows", - "raw-window-handle", - "winit", -] - -[[package]] -name = "adler2" -version = "2.0.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "320119579fcad9c21884f5c4861d16174d0e06250625266f50fe6898340abefa" - -[[package]] -name = "ahash" -version = "0.8.12" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5a15f179cd60c4584b8a8c596927aadc462e27f2ca70c04e0071964a73ba7a75" -dependencies = [ - "cfg-if", - "getrandom 0.3.4", - "once_cell", - "version_check", - "zerocopy", -] - -[[package]] -name = "aho-corasick" -version = "1.1.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ddd31a130427c27518df266943a5308ed92d4b226cc639f5a8f1002816174301" -dependencies = [ - "memchr", -] - -[[package]] -name = "aliasable" -version = "0.1.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "250f629c0161ad8107cf89319e990051fae62832fd343083bea452d93e2205fd" - -[[package]] -name = "aligned" -version = "0.4.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ee4508988c62edf04abd8d92897fca0c2995d907ce1dfeaf369dac3716a40685" -dependencies = [ - "as-slice", -] - -[[package]] -name = "aligned-vec" -version = "0.6.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "dc890384c8602f339876ded803c97ad529f3842aba97f6392b3dba0dd171769b" -dependencies = [ - "equator", -] - -[[package]] -name = "almost" -version = "0.2.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3aa2999eb46af81abb65c2d30d446778d7e613b60bbf4e174a027e80f90a3c14" - -[[package]] -name = "android-activity" -version = "0.6.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ef6978589202a00cd7e118380c448a08b6ed394c3a8df3a430d0898e3a42d046" -dependencies = [ - "android-properties", - "bitflags 2.10.0", - "cc", - "cesu8", - "jni", - "jni-sys", - "libc", - "log", - "ndk", - "ndk-context", - "ndk-sys 0.6.0+11769913", - "num_enum", - "thiserror 1.0.69", -] - -[[package]] -name = "android-properties" -version = "0.2.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "fc7eb209b1518d6bb87b283c20095f5228ecda460da70b44f0802523dea6da04" - -[[package]] -name = "android_system_properties" -version = "0.1.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "819e7219dbd41043ac279b19830f2efc897156490d7fd6ea916720117ee66311" -dependencies = [ - "libc", -] - -[[package]] -name = "anstream" -version = "0.6.21" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "43d5b281e737544384e969a5ccad3f1cdd24b48086a0fc1b2a5262a26b8f4f4a" -dependencies = [ - "anstyle", - "anstyle-parse", - "anstyle-query", - "anstyle-wincon", - "colorchoice", - "is_terminal_polyfill", - "utf8parse", -] - -[[package]] -name = "anstyle" -version = "1.0.13" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5192cca8006f1fd4f7237516f40fa183bb07f8fbdfedaa0036de5ea9b0b45e78" - -[[package]] -name = "anstyle-parse" -version = "0.2.7" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4e7644824f0aa2c7b9384579234ef10eb7efb6a0deb83f9630a49594dd9c15c2" -dependencies = [ - "utf8parse", -] - -[[package]] -name = "anstyle-query" -version = "1.1.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "40c48f72fd53cd289104fc64099abca73db4166ad86ea0b4341abe65af83dadc" -dependencies = [ - "windows-sys 0.61.2", -] - -[[package]] -name = "anstyle-wincon" -version = "3.0.11" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "291e6a250ff86cd4a820112fb8898808a366d8f9f58ce16d1f538353ad55747d" -dependencies = [ - "anstyle", - "once_cell_polyfill", - "windows-sys 0.61.2", -] - -[[package]] -name = "anyhow" -version = "1.0.100" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a23eb6b1614318a8071c9b2521f36b424b2c83db5eb3a0fead4a6c0809af6e61" - -[[package]] -name = "apply" -version = "0.3.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f47b57fc4521e3cae26a4d45b5227f8fadee4c345be0fefd8d5d1711afb8aeb9" - -[[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" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c3d036a3c4ab069c7b410a2ce876bd74808d2d0888a82667669f8e783a898bf1" - -[[package]] -name = "arc-swap" -version = "1.8.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "51d03449bb8ca2cc2ef70869af31463d1ae5ccc8fa3e334b307203fbf815207e" -dependencies = [ - "rustversion", -] - -[[package]] -name = "arg_enum_proc_macro" -version = "0.3.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0ae92a5119aa49cdbcf6b9f893fe4e1d98b04ccbf82ee0584ad948a44a734dea" -dependencies = [ - "proc-macro2", - "quote", - "syn 2.0.114", -] - -[[package]] -name = "arrayref" -version = "0.3.9" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "76a2e8124351fda1ef8aaaa3bbd7ebbcb486bbcd4225aca0aa0d84bb2db8fecb" - -[[package]] -name = "arrayvec" -version = "0.5.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "23b62fc65de8e4e7f52534fb52b0f3ed04746ae267519eef2a83941e8085068b" - -[[package]] -name = "arrayvec" -version = "0.7.6" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7c02d123df017efcdfbd739ef81735b36c5ba83ec3c59c80a9d7ecc718f92e50" - -[[package]] -name = "as-raw-xcb-connection" -version = "1.0.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "175571dd1d178ced59193a6fc02dde1b972eb0bc56c892cde9beeceac5bf0f6b" - -[[package]] -name = "as-slice" -version = "0.2.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "516b6b4f0e40d50dcda9365d53964ec74560ad4284da2e7fc97122cd83174516" -dependencies = [ - "stable_deref_trait", -] - -[[package]] -name = "ash" -version = "0.38.0+1.3.281" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0bb44936d800fea8f016d7f2311c6a4f97aebd5dc86f09906139ec848cf3a46f" -dependencies = [ - "libloading", -] - -[[package]] -name = "ashpd" -version = "0.11.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6cbdf310d77fd3aaee6ea2093db7011dc2d35d2eb3481e5607f1f8d942ed99df" -dependencies = [ - "enumflags2", - "futures-channel", - "futures-util", - "rand 0.9.2", - "raw-window-handle", - "serde", - "serde_repr", - "tokio", - "url", - "wayland-backend", - "wayland-client", - "wayland-protocols", - "zbus 5.13.0", -] - -[[package]] -name = "ashpd" -version = "0.12.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "da0986d5b4f0802160191ad75f8d33ada000558757db3defb70299ca95d9fcbd" -dependencies = [ - "enumflags2", - "futures-channel", - "futures-util", - "rand 0.9.2", - "serde", - "serde_repr", - "tokio", - "url", - "wayland-backend", - "wayland-client", - "wayland-protocols", - "zbus 5.13.0", -] - -[[package]] -name = "async-broadcast" -version = "0.5.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7c48ccdbf6ca6b121e0f586cbc0e73ae440e56c67c30fa0873b4e110d9c26d2b" -dependencies = [ - "event-listener 2.5.3", - "futures-core", -] - -[[package]] -name = "async-broadcast" -version = "0.7.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "435a87a52755b8f27fcf321ac4f04b2802e337c8c4872923137471ec39c37532" -dependencies = [ - "event-listener 5.4.1", - "event-listener-strategy", - "futures-core", - "pin-project-lite", -] - -[[package]] -name = "async-channel" -version = "2.5.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "924ed96dd52d1b75e9c1a3e6275715fd320f5f9439fb5a4a11fa51f4221158d2" -dependencies = [ - "concurrent-queue", - "event-listener-strategy", - "futures-core", - "pin-project-lite", -] - -[[package]] -name = "async-executor" -version = "1.13.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "497c00e0fd83a72a79a39fcbd8e3e2f055d6f6c7e025f3b3d91f4f8e76527fb8" -dependencies = [ - "async-task", - "concurrent-queue", - "fastrand 2.3.0", - "futures-lite 2.6.1", - "pin-project-lite", - "slab", -] - -[[package]] -name = "async-io" -version = "1.13.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0fc5b45d93ef0529756f812ca52e44c221b35341892d3dcc34132ac02f3dd2af" -dependencies = [ - "async-lock 2.8.0", - "autocfg", - "cfg-if", - "concurrent-queue", - "futures-lite 1.13.0", - "log", - "parking", - "polling 2.8.0", - "rustix 0.37.28", - "slab", - "socket2 0.4.10", - "waker-fn", -] - -[[package]] -name = "async-io" -version = "2.6.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "456b8a8feb6f42d237746d4b3e9a178494627745c3c56c6ea55d92ba50d026fc" -dependencies = [ - "autocfg", - "cfg-if", - "concurrent-queue", - "futures-io", - "futures-lite 2.6.1", - "parking", - "polling 3.11.0", - "rustix 1.1.3", - "slab", - "windows-sys 0.61.2", -] - -[[package]] -name = "async-lock" -version = "2.8.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "287272293e9d8c41773cec55e365490fe034813a2f172f502d6ddcf75b2f582b" -dependencies = [ - "event-listener 2.5.3", -] - -[[package]] -name = "async-lock" -version = "3.4.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "290f7f2596bd5b78a9fec8088ccd89180d7f9f55b94b0576823bbbdc72ee8311" -dependencies = [ - "event-listener 5.4.1", - "event-listener-strategy", - "pin-project-lite", -] - -[[package]] -name = "async-process" -version = "1.8.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ea6438ba0a08d81529c69b36700fa2f95837bfe3e776ab39cde9c14d9149da88" -dependencies = [ - "async-io 1.13.0", - "async-lock 2.8.0", - "async-signal", - "blocking", - "cfg-if", - "event-listener 3.1.0", - "futures-lite 1.13.0", - "rustix 0.38.44", - "windows-sys 0.48.0", -] - -[[package]] -name = "async-process" -version = "2.5.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "fc50921ec0055cdd8a16de48773bfeec5c972598674347252c0399676be7da75" -dependencies = [ - "async-channel", - "async-io 2.6.0", - "async-lock 3.4.2", - "async-signal", - "async-task", - "blocking", - "cfg-if", - "event-listener 5.4.1", - "futures-lite 2.6.1", - "rustix 1.1.3", -] - -[[package]] -name = "async-recursion" -version = "1.1.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3b43422f69d8ff38f95f1b2bb76517c91589a924d1559a0e935d7c8ce0274c11" -dependencies = [ - "proc-macro2", - "quote", - "syn 2.0.114", -] - -[[package]] -name = "async-signal" -version = "0.2.13" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "43c070bbf59cd3570b6b2dd54cd772527c7c3620fce8be898406dd3ed6adc64c" -dependencies = [ - "async-io 2.6.0", - "async-lock 3.4.2", - "atomic-waker", - "cfg-if", - "futures-core", - "futures-io", - "rustix 1.1.3", - "signal-hook-registry", - "slab", - "windows-sys 0.61.2", -] - -[[package]] -name = "async-task" -version = "4.7.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8b75356056920673b02621b35afd0f7dda9306d03c79a30f5c56c44cf256e3de" - -[[package]] -name = "async-trait" -version = "0.1.89" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9035ad2d096bed7955a320ee7e2230574d28fd3c3a0f186cbea1ff3c7eed5dbb" -dependencies = [ - "proc-macro2", - "quote", - "syn 2.0.114", -] - -[[package]] -name = "atomic-waker" -version = "1.1.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1505bd5d3d116872e7271a6d4e16d81d0c8570876c8de68093a09ac269d8aac0" - -[[package]] -name = "atomicwrites" -version = "0.4.2" -source = "git+https://github.com/jackpot51/rust-atomicwrites#043ab4859d53ffd3d55334685303d8df39c9f768" -dependencies = [ - "rustix 0.38.44", - "tempfile", - "windows-sys 0.48.0", -] - -[[package]] -name = "atspi" -version = "0.19.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6059f350ab6f593ea00727b334265c4dfc7fd442ee32d264794bd9bdc68e87ca" -dependencies = [ - "atspi-common", - "atspi-connection", - "atspi-proxies", -] - -[[package]] -name = "atspi-common" -version = "0.3.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "92af95f966d2431f962bc632c2e68eda7777330158bf640c4af4249349b2cdf5" -dependencies = [ - "enumflags2", - "serde", - "static_assertions", - "zbus 3.15.2", - "zbus_names 2.6.1", - "zvariant 3.15.2", -] - -[[package]] -name = "atspi-connection" -version = "0.3.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a0c65e7d70f86d4c0e3b2d585d9bf3f979f0b19d635a336725a88d279f76b939" -dependencies = [ - "atspi-common", - "atspi-proxies", - "futures-lite 1.13.0", - "zbus 3.15.2", -] - -[[package]] -name = "atspi-proxies" -version = "0.3.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6495661273703e7a229356dcbe8c8f38223d697aacfaf0e13590a9ac9977bb52" -dependencies = [ - "atspi-common", - "serde", - "zbus 3.15.2", -] - -[[package]] -name = "auto_enums" -version = "0.8.7" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9c170965892137a3a9aeb000b4524aa3cc022a310e709d848b6e1cdce4ab4781" -dependencies = [ - "derive_utils", - "proc-macro2", - "quote", - "syn 2.0.114", -] - -[[package]] -name = "autocfg" -version = "1.5.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c08606f8c3cbf4ce6ec8e28fb0014a2c086708fe954eaa885384a6165172e7e8" - -[[package]] -name = "av-scenechange" -version = "0.14.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0f321d77c20e19b92c39e7471cf986812cbb46659d2af674adc4331ef3f18394" -dependencies = [ - "aligned", - "anyhow", - "arg_enum_proc_macro", - "arrayvec 0.7.6", - "log", - "num-rational", - "num-traits", - "pastey", - "rayon", - "thiserror 2.0.17", - "v_frame", - "y4m", -] - -[[package]] -name = "av1-grain" -version = "0.2.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8cfddb07216410377231960af4fcab838eaa12e013417781b78bd95ee22077f8" -dependencies = [ - "anyhow", - "arrayvec 0.7.6", - "log", - "nom", - "num-rational", - "v_frame", -] - -[[package]] -name = "avif-serialize" -version = "0.8.6" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "47c8fbc0f831f4519fe8b810b6a7a91410ec83031b8233f730a0480029f6a23f" -dependencies = [ - "arrayvec 0.7.6", -] - -[[package]] -name = "base64" -version = "0.13.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9e1b586273c5702936fe7b7d6896644d8be71e6314cfe09d3167c95f712589e8" - -[[package]] -name = "base64" -version = "0.22.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "72b3254f16251a8381aa12e40e3c4d2f0199f8c6508fbecb9d91f575e0fbb8c6" - -[[package]] -name = "basic-toml" -version = "0.1.10" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ba62675e8242a4c4e806d12f11d136e626e6c8361d6b829310732241652a178a" -dependencies = [ - "serde", -] - -[[package]] -name = "bit-set" -version = "0.6.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f0481a0e032742109b1133a095184ee93d88f3dc9e0d28a5d033dc77a073f44f" -dependencies = [ - "bit-vec", -] - -[[package]] -name = "bit-vec" -version = "0.7.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d2c54ff287cfc0a34f38a6b832ea1bd8e448a330b3e40a50859e6488bee07f22" - -[[package]] -name = "bit_field" -version = "0.10.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1e4b40c7323adcfc0a41c4b88143ed58346ff65a288fc144329c5c45e05d70c6" - -[[package]] -name = "bitflags" -version = "1.3.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "bef38d45163c2f1dde094a7dfd33ccf595c92905c8f8f4fdc18d06fb1037718a" - -[[package]] -name = "bitflags" -version = "2.10.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "812e12b5285cc515a9c72a5c1d3b6d46a19dac5acfef5265968c166106e31dd3" -dependencies = [ - "serde_core", -] - -[[package]] -name = "bitstream-io" -version = "4.9.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "60d4bd9d1db2c6bdf285e223a7fa369d5ce98ec767dec949c6ca62863ce61757" -dependencies = [ - "core2", -] - -[[package]] -name = "blake2b_simd" -version = "0.5.11" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "afa748e348ad3be8263be728124b24a24f268266f6f5d58af9d75f6a40b5c587" -dependencies = [ - "arrayref", - "arrayvec 0.5.2", - "constant_time_eq", -] - -[[package]] -name = "block" -version = "0.1.6" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0d8c1fef690941d3e7788d328517591fecc684c084084702d6ff1641e993699a" - -[[package]] -name = "block-buffer" -version = "0.10.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3078c7629b62d3f0439517fa394996acacc5cbc91c5a20d8c658e77abd503a71" -dependencies = [ - "generic-array", -] - -[[package]] -name = "block2" -version = "0.5.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2c132eebf10f5cad5289222520a4a058514204aed6d791f1cf4fe8088b82d15f" -dependencies = [ - "objc2 0.5.2", -] - -[[package]] -name = "block2" -version = "0.6.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "cdeb9d870516001442e364c5220d3574d2da8dc765554b4a617230d33fa58ef5" -dependencies = [ - "objc2 0.6.3", -] - -[[package]] -name = "blocking" -version = "1.6.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e83f8d02be6967315521be875afa792a316e28d57b5a2d401897e2a7921b7f21" -dependencies = [ - "async-channel", - "async-task", - "futures-io", - "futures-lite 2.6.1", - "piper", -] - -[[package]] -name = "bstr" -version = "1.12.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "63044e1ae8e69f3b5a92c736ca6269b8d12fa7efe39bf34ddb06d102cf0e2cab" -dependencies = [ - "memchr", - "regex-automata", - "serde", -] - -[[package]] -name = "btoi" -version = "0.5.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3b5ab9db53bcda568284df0fd39f6eac24ad6f7ba7ff1168b9e76eba6576b976" -dependencies = [ - "num-traits", -] - -[[package]] -name = "built" -version = "0.8.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f4ad8f11f288f48ca24471bbd51ac257aaeaaa07adae295591266b792902ae64" - -[[package]] -name = "bumpalo" -version = "3.19.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5dd9dc738b7a8311c7ade152424974d8115f2cdad61e8dab8dac9f2362298510" - -[[package]] -name = "by_address" -version = "1.2.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "64fa3c856b712db6612c019f14756e64e4bcea13337a6b33b696333a9eaa2d06" - -[[package]] -name = "bytemuck" -version = "1.24.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1fbdf580320f38b612e485521afda1ee26d10cc9884efaaa750d383e13e3c5f4" -dependencies = [ - "bytemuck_derive", -] - -[[package]] -name = "bytemuck_derive" -version = "1.10.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f9abbd1bc6865053c427f7198e6af43bfdedc55ab791faed4fbd361d789575ff" -dependencies = [ - "proc-macro2", - "quote", - "syn 2.0.114", -] - -[[package]] -name = "byteorder" -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.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b35204fbdc0b3f4446b89fc1ac2cf84a8a68971995d0bf2e925ec7cd960f9cb3" - -[[package]] -name = "cairo-rs" -version = "0.18.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8ca26ef0159422fb77631dc9d17b102f253b876fe1586b03b803e63a309b4ee2" -dependencies = [ - "bitflags 2.10.0", - "cairo-sys-rs", - "glib", - "libc", - "once_cell", - "thiserror 1.0.69", -] - -[[package]] -name = "cairo-sys-rs" -version = "0.18.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "685c9fa8e590b8b3d678873528d83411db17242a73fccaed827770ea0fedda51" -dependencies = [ - "glib-sys", - "libc", - "system-deps", -] - -[[package]] -name = "calloop" -version = "0.13.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b99da2f8558ca23c71f4fd15dc57c906239752dd27ff3c00a1d56b685b7cbfec" -dependencies = [ - "bitflags 2.10.0", - "log", - "polling 3.11.0", - "rustix 0.38.44", - "slab", - "thiserror 1.0.69", -] - -[[package]] -name = "calloop" -version = "0.14.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "cb9f6e1368bd4621d2c86baa7e37de77a938adf5221e5dd3d6133340101b309e" -dependencies = [ - "bitflags 2.10.0", - "polling 3.11.0", - "rustix 1.1.3", - "slab", - "tracing", -] - -[[package]] -name = "calloop-wayland-source" -version = "0.3.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "95a66a987056935f7efce4ab5668920b5d0dac4a7c99991a67395f13702ddd20" -dependencies = [ - "calloop 0.13.0", - "rustix 0.38.44", - "wayland-backend", - "wayland-client", -] - -[[package]] -name = "calloop-wayland-source" -version = "0.4.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "138efcf0940a02ebf0cc8d1eff41a1682a46b431630f4c52450d6265876021fa" -dependencies = [ - "calloop 0.14.3", - "rustix 1.1.3", - "wayland-backend", - "wayland-client", -] - -[[package]] -name = "cc" -version = "1.2.52" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "cd4932aefd12402b36c60956a4fe0035421f544799057659ff86f923657aada3" -dependencies = [ - "find-msvc-tools", - "jobserver", - "libc", - "shlex", -] - -[[package]] -name = "cesu8" -version = "1.1.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6d43a04d8753f35258c91f8ec639f792891f748a1edbd759cf1dcea3382ad83c" - -[[package]] -name = "cfg-expr" -version = "0.15.8" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d067ad48b8650848b989a59a86c6c36a995d02d2bf778d45c3c5d57bc2718f02" -dependencies = [ - "smallvec", - "target-lexicon", -] - -[[package]] -name = "cfg-if" -version = "1.0.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 = "chrono" -version = "0.4.42" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "145052bdd345b87320e369255277e3fb5152762ad123a901ef5c262dd38fe8d2" -dependencies = [ - "iana-time-zone", - "js-sys", - "num-traits", - "wasm-bindgen", - "windows-link", -] - -[[package]] -name = "clap" -version = "4.5.54" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c6e6ff9dcd79cff5cd969a17a545d79e84ab086e444102a591e288a8aa3ce394" -dependencies = [ - "clap_builder", - "clap_derive", -] - -[[package]] -name = "clap_builder" -version = "4.5.54" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "fa42cf4d2b7a41bc8f663a7cab4031ebafa1bf3875705bfaf8466dc60ab52c00" -dependencies = [ - "anstream", - "anstyle", - "clap_lex", - "strsim", -] - -[[package]] -name = "clap_derive" -version = "4.5.49" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2a0b5487afeab2deb2ff4e03a807ad1a03ac532ff5a2cee5d86884440c7f7671" -dependencies = [ - "heck 0.5.0", - "proc-macro2", - "quote", - "syn 2.0.114", -] - -[[package]] -name = "clap_lex" -version = "0.7.6" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a1d728cc89cf3aee9ff92b05e62b19ee65a02b5702cff7d5a377e32c6ae29d8d" - -[[package]] -name = "clipboard-win" -version = "5.4.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "bde03770d3df201d4fb868f2c9c59e66a3e4e2bd06692a0fe701e7103c7e84d4" -dependencies = [ - "error-code", -] - -[[package]] -name = "clipboard_macos" -version = "0.1.0" -source = "git+https://github.com/pop-os/window_clipboard.git?tag=pop-0.13-2#6b9faab87bea9cebec6ae036906fd67fed254f5f" -dependencies = [ - "objc", - "objc-foundation", - "objc_id", -] - -[[package]] -name = "clipboard_wayland" -version = "0.2.2" -source = "git+https://github.com/pop-os/window_clipboard.git?tag=pop-0.13-2#6b9faab87bea9cebec6ae036906fd67fed254f5f" -dependencies = [ - "dnd", - "mime", - "smithay-clipboard", -] - -[[package]] -name = "clipboard_x11" -version = "0.4.2" -source = "git+https://github.com/pop-os/window_clipboard.git?tag=pop-0.13-2#6b9faab87bea9cebec6ae036906fd67fed254f5f" -dependencies = [ - "thiserror 1.0.69", - "x11rb", -] - -[[package]] -name = "cocoa" -version = "0.25.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f6140449f97a6e97f9511815c5632d84c8aacf8ac271ad77c559218161a1373c" -dependencies = [ - "bitflags 1.3.2", - "block", - "cocoa-foundation", - "core-foundation", - "core-graphics", - "foreign-types", - "libc", - "objc", -] - -[[package]] -name = "cocoa-foundation" -version = "0.1.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8c6234cbb2e4c785b456c0644748b1ac416dd045799740356f8363dfe00c93f7" -dependencies = [ - "bitflags 1.3.2", - "block", - "core-foundation", - "core-graphics-types", - "libc", - "objc", -] - -[[package]] -name = "codespan-reporting" -version = "0.11.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3538270d33cc669650c4b093848450d380def10c331d38c768e34cac80576e6e" -dependencies = [ - "termcolor", - "unicode-width", -] - -[[package]] -name = "color_quant" -version = "1.1.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3d7b894f5411737b7867f4827955924d7c254fc9f4d91a6aad6b097804b1018b" - -[[package]] -name = "colorchoice" -version = "1.0.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b05b61dc5112cbb17e4b6cd61790d9845d13888356391624cbe7e41efeac1e75" - -[[package]] -name = "colored" -version = "2.2.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "117725a109d387c937a1533ce01b450cbde6b88abceea8473c4d7a85853cda3c" -dependencies = [ - "lazy_static", - "windows-sys 0.59.0", -] - -[[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" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ba5a308b75df32fe02788e748662718f03fde005016435c444eea572398219fd" -dependencies = [ - "bytes", - "memchr", -] - -[[package]] -name = "concurrent-queue" -version = "2.5.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4ca0197aee26d1ae37445ee532fefce43251d24cc7c166799f4d46817f1d3973" -dependencies = [ - "crossbeam-utils", -] - -[[package]] -name = "constant_time_eq" -version = "0.1.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "245097e9a4535ee1e3e3931fcfcd55a796a44c643e8596ff6566d68f09b87bbc" - -[[package]] -name = "core-foundation" -version = "0.9.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "91e195e091a93c46f7102ec7818a2aa394e1e1771c3ab4825963fa03e45afb8f" -dependencies = [ - "core-foundation-sys", - "libc", -] - -[[package]] -name = "core-foundation-sys" -version = "0.8.7" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "773648b94d0e5d620f64f280777445740e61fe701025087ec8b57f45c791888b" - -[[package]] -name = "core-graphics" -version = "0.23.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c07782be35f9e1140080c6b96f0d44b739e2278479f64e02fdab4e32dfd8b081" -dependencies = [ - "bitflags 1.3.2", - "core-foundation", - "core-graphics-types", - "foreign-types", - "libc", -] - -[[package]] -name = "core-graphics-types" -version = "0.1.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "45390e6114f68f718cc7a830514a96f903cccd70d02a8f6d9f643ac4ba45afaf" -dependencies = [ - "bitflags 1.3.2", - "core-foundation", - "libc", -] - -[[package]] -name = "core2" -version = "0.4.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b49ba7ef1ad6107f8824dbe97de947cbaac53c44e7f9756a1fba0d37c1eec505" -dependencies = [ - "memchr", -] - -[[package]] -name = "core_maths" -version = "0.1.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "77745e017f5edba1a9c1d854f6f3a52dac8a12dd5af5d2f54aecf61e43d80d30" -dependencies = [ - "libm", -] - -[[package]] -name = "cosmic-client-toolkit" -version = "0.1.0" -source = "git+https://github.com/pop-os/cosmic-protocols?rev=d0e95be#d0e95be25e423cfe523b11111a3666ed7aaf0dc4" -dependencies = [ - "bitflags 2.10.0", - "cosmic-protocols", - "libc", - "smithay-client-toolkit 0.20.0", - "wayland-client", - "wayland-protocols", -] - -[[package]] -name = "cosmic-config" -version = "0.1.0" -source = "git+https://github.com/pop-os/libcosmic.git#b9c24d24212a865977db4871efc13ff890055648" -dependencies = [ - "atomicwrites", - "cosmic-config-derive", - "cosmic-settings-daemon", - "dirs 6.0.0", - "futures-util", - "iced_futures", - "known-folders", - "notify", - "ron", - "serde", - "tokio", - "tracing", - "xdg", - "zbus 5.13.0", -] - -[[package]] -name = "cosmic-config-derive" -version = "0.1.0" -source = "git+https://github.com/pop-os/libcosmic.git#b9c24d24212a865977db4871efc13ff890055648" -dependencies = [ - "quote", - "syn 2.0.114", -] - -[[package]] -name = "cosmic-freedesktop-icons" -version = "0.4.0" -source = "git+https://github.com/pop-os/freedesktop-icons#7a61a704f6d1ec41f71cbe766e3cc484858523fa" -dependencies = [ - "bstr", - "btoi", - "memchr", - "memmap2 0.9.9", - "thiserror 2.0.17", - "tracing", - "xdg", -] - -[[package]] -name = "cosmic-protocols" -version = "0.1.0" -source = "git+https://github.com/pop-os/cosmic-protocols?rev=d0e95be#d0e95be25e423cfe523b11111a3666ed7aaf0dc4" -dependencies = [ - "bitflags 2.10.0", - "wayland-backend", - "wayland-client", - "wayland-protocols", - "wayland-protocols-wlr", - "wayland-scanner", - "wayland-server", -] - -[[package]] -name = "cosmic-settings-daemon" -version = "0.1.0" -source = "git+https://github.com/pop-os/dbus-settings-bindings#87c3c35666b926a24a1e8045fd70be2db1145e34" -dependencies = [ - "zbus 5.13.0", -] - -[[package]] -name = "cosmic-text" -version = "0.16.0" -source = "git+https://github.com/pop-os/cosmic-text.git#0d9af4f7de087878100b296c81d1baca2e05433d" -dependencies = [ - "bitflags 2.10.0", - "fontdb 0.23.0", - "harfrust", - "linebender_resource_handle", - "log", - "rangemap", - "rustc-hash 1.1.0", - "self_cell", - "skrifa 0.39.0", - "smol_str", - "swash", - "sys-locale", - "unicode-bidi", - "unicode-linebreak", - "unicode-script", - "unicode-segmentation", -] - -[[package]] -name = "cosmic-theme" -version = "0.1.0" -source = "git+https://github.com/pop-os/libcosmic.git#b9c24d24212a865977db4871efc13ff890055648" -dependencies = [ - "almost", - "cosmic-config", - "csscolorparser", - "dirs 6.0.0", - "palette", - "ron", - "serde", - "serde_json", - "thiserror 2.0.17", -] - -[[package]] -name = "cpufeatures" -version = "0.2.17" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "59ed5838eebb26a2bb2e58f6d5b5316989ae9d08bab10e0e6d103e656d1b0280" -dependencies = [ - "libc", -] - -[[package]] -name = "crc32fast" -version = "1.5.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9481c1c90cbf2ac953f07c8d4a58aa3945c425b7185c9154d67a65e4230da511" -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" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d0a5c400df2834b80a4c3327b3aad3a4c4cd4de0629063962b03235697506a28" - -[[package]] -name = "crunchy" -version = "0.2.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "460fbee9c2c2f33933d720630a6a0bac33ba7053db5344fac858d4b8952d77d5" - -[[package]] -name = "crypto-common" -version = "0.1.7" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "78c8292055d1c1df0cce5d180393dc8cce0abec0a7102adb6c7b1eef6016d60a" -dependencies = [ - "generic-array", - "typenum", -] - -[[package]] -name = "css-color" -version = "0.2.8" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "42aaeae719fd78ce501d77c6cdf01f7e96f26bcd5617a4903a1c2b97e388543a" - -[[package]] -name = "csscolorparser" -version = "0.7.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5fda6aace1fbef3aa217b27f4c8d7d071ef2a70a5ca51050b1f17d40299d3f16" -dependencies = [ - "phf 0.11.3", - "serde", -] - -[[package]] -name = "ctor-lite" -version = "0.1.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1f791803201ab277ace03903de1594460708d2d54df6053f2d9e82f592b19e3b" - -[[package]] -name = "cursor-icon" -version = "1.2.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f27ae1dd37df86211c42e150270f82743308803d90a6f6e6651cd730d5e1732f" - -[[package]] -name = "d3d12" -version = "22.0.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "bdbd1f579714e3c809ebd822c81ef148b1ceaeb3d535352afc73fd0c4c6a0017" -dependencies = [ - "bitflags 2.10.0", - "libloading", - "winapi", -] - -[[package]] -name = "darling" -version = "0.20.11" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "fc7f46116c46ff9ab3eb1597a45688b6715c6e628b5c133e288e709a29bcb4ee" -dependencies = [ - "darling_core", - "darling_macro", -] - -[[package]] -name = "darling_core" -version = "0.20.11" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0d00b9596d185e565c2207a0b01f8bd1a135483d02d9b7b0a54b11da8d53412e" -dependencies = [ - "fnv", - "ident_case", - "proc-macro2", - "quote", - "strsim", - "syn 2.0.114", -] - -[[package]] -name = "darling_macro" -version = "0.20.11" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "fc34b93ccb385b40dc71c6fceac4b2ad23662c7eeb248cf10d529b7e055b6ead" -dependencies = [ - "darling_core", - "quote", - "syn 2.0.114", -] - -[[package]] -name = "data-url" -version = "0.3.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "be1e0bca6c3637f992fc1cc7cbc52a78c1ef6db076dbf1059c4323d6a2048376" - -[[package]] -name = "deranged" -version = "0.5.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ececcb659e7ba858fb4f10388c250a7252eb0a27373f1a72b8748afdd248e587" -dependencies = [ - "powerfmt", -] - -[[package]] -name = "derivative" -version = "2.2.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "fcc3dd5e9e9c0b295d6e1e4d811fb6f157d5ffd784b8d202fc62eac8035a770b" -dependencies = [ - "proc-macro2", - "quote", - "syn 1.0.109", -] - -[[package]] -name = "derive_setters" -version = "0.1.8" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ae5c625eda104c228c06ecaf988d1c60e542176bd7a490e60eeda3493244c0c9" -dependencies = [ - "darling", - "proc-macro2", - "quote", - "syn 2.0.114", -] - -[[package]] -name = "derive_utils" -version = "0.15.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ccfae181bab5ab6c5478b2ccb69e4c68a02f8c3ec72f6616bfec9dbc599d2ee0" -dependencies = [ - "proc-macro2", - "quote", - "syn 2.0.114", -] - -[[package]] -name = "digest" -version = "0.10.7" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9ed9a281f7bc9b7576e61468ba615a66a5c8cfdff42420a70aa82701a3b1e292" -dependencies = [ - "block-buffer", - "crypto-common", -] - -[[package]] -name = "dirs" -version = "1.0.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3fd78930633bd1c6e35c4b42b1df7b0cbc6bc191146e512bb3bedf243fcc3901" -dependencies = [ - "libc", - "redox_users 0.3.5", - "winapi", -] - -[[package]] -name = "dirs" -version = "5.0.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "44c45a9d03d6676652bcb5e724c7e988de1acad23a711b5217ab9cbecbec2225" -dependencies = [ - "dirs-sys 0.4.1", -] - -[[package]] -name = "dirs" -version = "6.0.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c3e8aa94d75141228480295a7d0e7feb620b1a5ad9f12bc40be62411e38cce4e" -dependencies = [ - "dirs-sys 0.5.0", -] - -[[package]] -name = "dirs-sys" -version = "0.4.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "520f05a5cbd335fae5a99ff7a6ab8627577660ee5cfd6a94a6a929b52ff0321c" -dependencies = [ - "libc", - "option-ext", - "redox_users 0.4.6", - "windows-sys 0.48.0", -] - -[[package]] -name = "dirs-sys" -version = "0.5.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e01a3366d27ee9890022452ee61b2b63a67e6f13f58900b651ff5665f0bb1fab" -dependencies = [ - "libc", - "option-ext", - "redox_users 0.5.2", - "windows-sys 0.61.2", -] - -[[package]] -name = "dispatch" -version = "0.2.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "bd0c93bb4b0c6d9b77f4435b0ae98c24d17f1c45b2ff844c6151a07256ca923b" - -[[package]] -name = "dispatch2" -version = "0.3.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "89a09f22a6c6069a18470eb92d2298acf25463f14256d24778e1230d789a2aec" -dependencies = [ - "bitflags 2.10.0", - "block2 0.6.2", - "libc", - "objc2 0.6.3", -] - -[[package]] -name = "displaydoc" -version = "0.2.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "97369cbbc041bc366949bc74d34658d6cda5621039731c6310521892a3a20ae0" -dependencies = [ - "proc-macro2", - "quote", - "syn 2.0.114", -] - -[[package]] -name = "dlib" -version = "0.5.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "330c60081dcc4c72131f8eb70510f1ac07223e5d4163db481a04a0befcffa412" -dependencies = [ - "libloading", -] - -[[package]] -name = "dnd" -version = "0.1.0" -source = "git+https://github.com/pop-os/window_clipboard.git?tag=pop-0.13-2#6b9faab87bea9cebec6ae036906fd67fed254f5f" -dependencies = [ - "bitflags 2.10.0", - "mime", - "raw-window-handle", - "smithay-client-toolkit 0.19.2", - "smithay-clipboard", -] - -[[package]] -name = "document-features" -version = "0.2.12" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d4b8a88685455ed29a21542a33abd9cb6510b6b129abadabdcef0f4c55bc8f61" -dependencies = [ - "litrs", -] - -[[package]] -name = "downcast-rs" -version = "1.2.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "75b325c5dbd37f80359721ad39aca5a29fb04c89279657cffdda8736d0c0b9d2" - -[[package]] -name = "dpi" -version = "0.1.1" -source = "git+https://github.com/pop-os/winit.git?tag=iced-xdg-surface-0.13-rc#12a5f17d1811cdebbcbd310a3d92965e9142fa12" - -[[package]] -name = "drm" -version = "0.11.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a0f8a69e60d75ae7dab4ef26a59ca99f2a89d4c142089b537775ae0c198bdcde" -dependencies = [ - "bitflags 2.10.0", - "bytemuck", - "drm-ffi", - "drm-fourcc", - "rustix 0.38.44", -] - -[[package]] -name = "drm-ffi" -version = "0.7.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "41334f8405792483e32ad05fbb9c5680ff4e84491883d2947a4757dc54cb2ac6" -dependencies = [ - "drm-sys", - "rustix 0.38.44", -] - -[[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.6.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2d09ff881f92f118b11105ba5e34ff8f4adf27b30dae8f12e28c193af1c83176" -dependencies = [ - "libc", - "linux-raw-sys 0.6.5", -] - -[[package]] -name = "either" -version = "1.15.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "48c757948c5ede0e46177b7add2e67155f70e33c07fea8284df6576da70b3719" - -[[package]] -name = "endi" -version = "1.1.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "66b7e2430c6dff6a955451e2cfc438f09cea1965a9d6f87f7e3b90decc014099" - -[[package]] -name = "enquote" -version = "1.1.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "06c36cb11dbde389f4096111698d8b567c0720e3452fd5ac3e6b4e47e1939932" -dependencies = [ - "thiserror 1.0.69", -] - -[[package]] -name = "enumflags2" -version = "0.7.12" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1027f7680c853e056ebcec683615fb6fbbc07dbaa13b4d5d9442b146ded4ecef" -dependencies = [ - "enumflags2_derive", - "serde", -] - -[[package]] -name = "enumflags2_derive" -version = "0.7.12" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "67c78a4d8fdf9953a5c9d458f9efe940fd97a0cab0941c075a813ac594733827" -dependencies = [ - "proc-macro2", - "quote", - "syn 2.0.114", -] - -[[package]] -name = "env_filter" -version = "0.1.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1bf3c259d255ca70051b30e2e95b5446cdb8949ac4cd22c0d7fd634d89f568e2" -dependencies = [ - "log", - "regex", -] - -[[package]] -name = "env_logger" -version = "0.11.8" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "13c863f0904021b108aa8b2f55046443e6b1ebde8fd4a15c399893aae4fa069f" -dependencies = [ - "anstream", - "anstyle", - "env_filter", - "jiff", - "log", -] - -[[package]] -name = "equator" -version = "0.4.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4711b213838dfee0117e3be6ac926007d7f433d7bbe33595975d4190cb07e6fc" -dependencies = [ - "equator-macro", -] - -[[package]] -name = "equator-macro" -version = "0.4.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "44f23cf4b44bfce11a86ace86f8a73ffdec849c9fd00a386a53d278bd9e81fb3" -dependencies = [ - "proc-macro2", - "quote", - "syn 2.0.114", -] - -[[package]] -name = "equivalent" -version = "1.0.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "877a4ace8713b0bcf2a4e7eec82529c029f1d0619886d18145fea96c3ffe5c0f" - -[[package]] -name = "errno" -version = "0.3.14" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "39cab71617ae0d63f51a36d69f866391735b51691dbda63cf6f96d042b63efeb" -dependencies = [ - "libc", - "windows-sys 0.61.2", -] - -[[package]] -name = "error-code" -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.11" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ad9cdb4b747e485a12abb0e6566612956c7a1bafa3bdb8d682c5b6d403589e48" -dependencies = [ - "num-traits", -] - -[[package]] -name = "event-listener" -version = "2.5.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0206175f82b8d6bf6652ff7d71a1e27fd2e4efde587fd368662814d6ec1d9ce0" - -[[package]] -name = "event-listener" -version = "3.1.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d93877bcde0eb80ca09131a08d23f0a5c18a620b01db137dba666d18cd9b30c2" -dependencies = [ - "concurrent-queue", - "parking", - "pin-project-lite", -] - -[[package]] -name = "event-listener" -version = "5.4.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e13b66accf52311f30a0db42147dadea9850cb48cd070028831ae5f5d4b856ab" -dependencies = [ - "concurrent-queue", - "parking", - "pin-project-lite", -] - -[[package]] -name = "event-listener-strategy" -version = "0.5.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8be9f3dfaaffdae2972880079a491a1a8bb7cbed0b8dd7a347f668b4150a3b93" -dependencies = [ - "event-listener 5.4.1", - "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 = "fast-srgb8" -version = "1.0.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "dd2e7510819d6fbf51a5545c8f922716ecfb14df168a3242f7d33e0239efe6a1" - -[[package]] -name = "fastrand" -version = "1.9.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e51093e27b0797c359783294ca4f0a911c270184cb10f85783b118614a1501be" -dependencies = [ - "instant", -] - -[[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.114", -] - -[[package]] -name = "fdeflate" -version = "0.3.7" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1e6853b52649d4ac5c0bd02320cddc5ba956bdb407c4b75a2c6b75bf51500f8c" -dependencies = [ - "simd-adler32", -] - -[[package]] -name = "find-crate" -version = "0.6.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "59a98bbaacea1c0eb6a0876280051b892eb73594fd90cf3b20e9c817029c57d2" -dependencies = [ - "toml 0.5.11", -] - -[[package]] -name = "find-msvc-tools" -version = "0.1.7" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f449e6c6c08c865631d4890cfacf252b3d396c9bcc83adb6623cdb02a8336c41" - -[[package]] -name = "flate2" -version = "1.1.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "bfe33edd8e85a12a67454e37f8c75e730830d83e313556ab9ebf9ee7fbeb3bfb" -dependencies = [ - "crc32fast", - "miniz_oxide", -] - -[[package]] -name = "float-cmp" -version = "0.9.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "98de4bbd547a563b716d8dfa9aad1cb19bfab00f4fa09a6a4ed21dbcf44ce9c4" - -[[package]] -name = "float_next_after" -version = "1.0.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8bf7cc16383c4b8d58b9905a8509f02926ce3058053c056376248d958c9df1e8" - -[[package]] -name = "fluent" -version = "0.17.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8137a6d5a2c50d6b0ebfcb9aaa91a28154e0a70605f112d30cb0cd4a78670477" -dependencies = [ - "fluent-bundle", - "unic-langid", -] - -[[package]] -name = "fluent-bundle" -version = "0.16.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "01203cb8918f5711e73891b347816d932046f95f54207710bda99beaeb423bf4" -dependencies = [ - "fluent-langneg", - "fluent-syntax", - "intl-memoizer", - "intl_pluralrules", - "rustc-hash 2.1.1", - "self_cell", - "smallvec", - "unic-langid", -] - -[[package]] -name = "fluent-langneg" -version = "0.13.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7eebbe59450baee8282d71676f3bfed5689aeab00b27545e83e5f14b1195e8b0" -dependencies = [ - "unic-langid", -] - -[[package]] -name = "fluent-syntax" -version = "0.12.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "54f0d287c53ffd184d04d8677f590f4ac5379785529e5e08b1c8083acdd5c198" -dependencies = [ - "memchr", - "thiserror 2.0.17", -] - -[[package]] -name = "fnv" -version = "1.0.7" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3f9eec918d3f24069decb9af1554cad7c880e2da24a9afd88aca000531ab82c1" - -[[package]] -name = "foldhash" -version = "0.1.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d9c4f5dac5e15c24eb999c26181a6ca40b39fe946cbe4c263c7209467bc83af2" - -[[package]] -name = "font-types" -version = "0.10.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "39a654f404bbcbd48ea58c617c2993ee91d1cb63727a37bf2323a4edeed1b8c5" -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.18.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e32eac81c1135c1df01d4e6d4233c47ba11f6a6d07f33e0bba09d18797077770" -dependencies = [ - "fontconfig-parser", - "log", - "memmap2 0.9.9", - "slotmap", - "tinyvec", - "ttf-parser 0.21.1", -] - -[[package]] -name = "fontdb" -version = "0.23.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "457e789b3d1202543297a350643cf459f836cade38934e7a4cf6a39e7cde2905" -dependencies = [ - "fontconfig-parser", - "log", - "memmap2 0.9.9", - "slotmap", - "tinyvec", - "ttf-parser 0.25.1", -] - -[[package]] -name = "foreign-types" -version = "0.5.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d737d9aa519fb7b749cbc3b962edcf310a8dd1f4b67c91c4f83975dbdd17d965" -dependencies = [ - "foreign-types-macros", - "foreign-types-shared", -] - -[[package]] -name = "foreign-types-macros" -version = "0.2.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1a5c6c585bc94aaf2c7b51dd4c2ba22680844aba4c687be581871a6f518c5742" -dependencies = [ - "proc-macro2", - "quote", - "syn 2.0.114", -] - -[[package]] -name = "foreign-types-shared" -version = "0.3.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "aa9a19cbb55df58761df49b23516a86d432839add4af60fc256da840f66ed35b" - -[[package]] -name = "form_urlencoded" -version = "1.2.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "cb4cb245038516f5f85277875cdaa4f7d2c9a0fa0468de06ed190163b1581fcf" -dependencies = [ - "percent-encoding", -] - -[[package]] -name = "fsevent-sys" -version = "4.1.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "76ee7a02da4d231650c7cea31349b889be2f45ddb3ef3032d2ec8185f6313fd2" -dependencies = [ - "libc", -] - -[[package]] -name = "futures" -version = "0.3.31" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "65bc07b1a8bc7c85c5f2e110c476c7389b4554ba72af57d8445ea63a576b0876" -dependencies = [ - "futures-channel", - "futures-core", - "futures-executor", - "futures-io", - "futures-sink", - "futures-task", - "futures-util", -] - -[[package]] -name = "futures-channel" -version = "0.3.31" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2dff15bf788c671c1934e366d07e30c1814a8ef514e1af724a602e8a2fbe1b10" -dependencies = [ - "futures-core", - "futures-sink", -] - -[[package]] -name = "futures-core" -version = "0.3.31" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "05f29059c0c2090612e8d742178b0580d2dc940c837851ad723096f87af6663e" - -[[package]] -name = "futures-executor" -version = "0.3.31" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1e28d1d997f585e54aebc3f97d39e72338912123a67330d723fdbb564d646c9f" -dependencies = [ - "futures-core", - "futures-task", - "futures-util", - "num_cpus", -] - -[[package]] -name = "futures-io" -version = "0.3.31" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9e5c1b78ca4aae1ac06c48a526a655760685149f0d465d21f37abfe57ce075c6" - -[[package]] -name = "futures-lite" -version = "1.13.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "49a9d51ce47660b1e808d3c990b4709f2f415d928835a17dfd16991515c46bce" -dependencies = [ - "fastrand 1.9.0", - "futures-core", - "futures-io", - "memchr", - "parking", - "pin-project-lite", - "waker-fn", -] - -[[package]] -name = "futures-lite" -version = "2.6.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f78e10609fe0e0b3f4157ffab1876319b5b0db102a2c60dc4626306dc46b44ad" -dependencies = [ - "fastrand 2.3.0", - "futures-core", - "futures-io", - "parking", - "pin-project-lite", -] - -[[package]] -name = "futures-macro" -version = "0.3.31" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "162ee34ebcb7c64a8abebc059ce0fee27c2262618d7b60ed8faf72fef13c3650" -dependencies = [ - "proc-macro2", - "quote", - "syn 2.0.114", -] - -[[package]] -name = "futures-sink" -version = "0.3.31" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e575fab7d1e0dcb8d0c7bcf9a63ee213816ab51902e6d244a95819acacf1d4f7" - -[[package]] -name = "futures-task" -version = "0.3.31" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f90f7dce0722e95104fcb095585910c0977252f286e354b5e3bd38902cd99988" - -[[package]] -name = "futures-util" -version = "0.3.31" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9fa08315bb612088cc391249efdc3bc77536f16c91f6cf495e6fbe85b20a4a81" -dependencies = [ - "futures-channel", - "futures-core", - "futures-io", - "futures-macro", - "futures-sink", - "futures-task", - "memchr", - "pin-project-lite", - "pin-utils", - "slab", -] - -[[package]] -name = "generic-array" -version = "0.14.7" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "85649ca51fd72272d7821adaf274ad91c288277713d9c18820d8499a7ff69e9a" -dependencies = [ - "typenum", - "version_check", -] - -[[package]] -name = "gethostname" -version = "1.1.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1bd49230192a3797a9a4d6abe9b3eed6f7fa4c8a8a4947977c6f80025f92cbd8" -dependencies = [ - "rustix 1.1.3", - "windows-link", -] - -[[package]] -name = "getrandom" -version = "0.1.16" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8fc3cb4d91f53b50155bdcfd23f6a4c39ae1969c2ae85982b135750cccaf5fce" -dependencies = [ - "cfg-if", - "libc", - "wasi 0.9.0+wasi-snapshot-preview1", -] - -[[package]] -name = "getrandom" -version = "0.2.16" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "335ff9f135e4384c8150d6f27c6daed433577f86b4750418338c01a1a2528592" -dependencies = [ - "cfg-if", - "libc", - "wasi 0.11.1+wasi-snapshot-preview1", -] - -[[package]] -name = "getrandom" -version = "0.3.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "899def5c37c4fd7b2664648c28120ecec138e4d395b459e5ca34f9cce2dd77fd" -dependencies = [ - "cfg-if", - "libc", - "r-efi", - "wasip2", -] - -[[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 = "gif" -version = "0.14.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f5df2ba84018d80c213569363bdcd0c64e6933c67fe4c1d60ecf822971a3c35e" -dependencies = [ - "color_quant", - "weezl", -] - -[[package]] -name = "gio-sys" -version = "0.18.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "37566df850baf5e4cb0dfb78af2e4b9898d817ed9263d1090a2df958c64737d2" -dependencies = [ - "glib-sys", - "gobject-sys", - "libc", - "system-deps", - "winapi", -] - -[[package]] -name = "gl_generator" -version = "0.14.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1a95dfc23a2b4a9a2f5ab41d194f8bfda3cabec42af4e39f08c339eb2a0c124d" -dependencies = [ - "khronos_api", - "log", - "xml-rs", -] - -[[package]] -name = "glam" -version = "0.25.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "151665d9be52f9bb40fc7966565d39666f2d1e69233571b71b87791c7e0528b3" - -[[package]] -name = "glib" -version = "0.18.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "233daaf6e83ae6a12a52055f568f9d7cf4671dabb78ff9560ab6da230ce00ee5" -dependencies = [ - "bitflags 2.10.0", - "futures-channel", - "futures-core", - "futures-executor", - "futures-task", - "futures-util", - "gio-sys", - "glib-macros", - "glib-sys", - "gobject-sys", - "libc", - "memchr", - "once_cell", - "smallvec", - "thiserror 1.0.69", -] - -[[package]] -name = "glib-macros" -version = "0.18.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0bb0228f477c0900c880fd78c8759b95c7636dbd7842707f49e132378aa2acdc" -dependencies = [ - "heck 0.4.1", - "proc-macro-crate 2.0.2", - "proc-macro-error", - "proc-macro2", - "quote", - "syn 2.0.114", -] - -[[package]] -name = "glib-sys" -version = "0.18.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "063ce2eb6a8d0ea93d2bf8ba1957e78dbab6be1c2220dd3daca57d5a9d869898" -dependencies = [ - "libc", - "system-deps", -] - -[[package]] -name = "glow" -version = "0.13.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "bd348e04c43b32574f2de31c8bb397d96c9fcfa1371bd4ca6d8bdc464ab121b1" -dependencies = [ - "js-sys", - "slotmap", - "wasm-bindgen", - "web-sys", -] - -[[package]] -name = "glutin_wgl_sys" -version = "0.6.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2c4ee00b289aba7a9e5306d57c2d05499b2e5dc427f84ac708bd2c090212cf3e" -dependencies = [ - "gl_generator", -] - -[[package]] -name = "gobject-sys" -version = "0.18.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0850127b514d1c4a4654ead6dedadb18198999985908e6ffe4436f53c785ce44" -dependencies = [ - "glib-sys", - "libc", - "system-deps", -] - -[[package]] -name = "gpu-alloc" -version = "0.6.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "fbcd2dba93594b227a1f57ee09b8b9da8892c34d55aa332e034a228d0fe6a171" -dependencies = [ - "bitflags 2.10.0", - "gpu-alloc-types", -] - -[[package]] -name = "gpu-alloc-types" -version = "0.3.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "98ff03b468aa837d70984d55f5d3f846f6ec31fe34bbb97c4f85219caeee1ca4" -dependencies = [ - "bitflags 2.10.0", -] - -[[package]] -name = "gpu-allocator" -version = "0.26.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "fdd4240fc91d3433d5e5b0fc5b67672d771850dc19bbee03c1381e19322803d7" -dependencies = [ - "log", - "presser", - "thiserror 1.0.69", - "winapi", - "windows 0.52.0", -] - -[[package]] -name = "gpu-descriptor" -version = "0.3.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b89c83349105e3732062a895becfc71a8f921bb71ecbbdd8ff99263e3b53a0ca" -dependencies = [ - "bitflags 2.10.0", - "gpu-descriptor-types", - "hashbrown 0.15.5", -] - -[[package]] -name = "gpu-descriptor-types" -version = "0.2.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "fdf242682df893b86f33a73828fb09ca4b2d3bb6cc95249707fc684d27484b91" -dependencies = [ - "bitflags 2.10.0", -] - -[[package]] -name = "grid" -version = "1.0.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f9e2d4c0a8296178d8802098410ca05d86b17a10bb5ab559b3fb404c1f948220" - -[[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" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6ea2d84b969582b4b1864a92dc5d27cd2b77b622a8d79306834f1be5ba20d84b" -dependencies = [ - "cfg-if", - "crunchy", - "zerocopy", -] - -[[package]] -name = "harfrust" -version = "0.4.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e0caaee032384c10dd597af4579c67dee16650d862a9ccbe1233ff1a379abc07" -dependencies = [ - "bitflags 2.10.0", - "bytemuck", - "core_maths", - "read-fonts 0.36.0", - "smallvec", -] - -[[package]] -name = "hashbrown" -version = "0.15.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9229cfe53dfd69f0609a49f65461bd93001ea1ef889cd5529dd176593f5338a1" -dependencies = [ - "foldhash", -] - -[[package]] -name = "hashbrown" -version = "0.16.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "841d1cc9bed7f9236f321df977030373f4a4163ae1a7dbfe1a51a2c1a51d9100" - -[[package]] -name = "hassle-rs" -version = "0.11.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "af2a7e73e1f34c48da31fb668a907f250794837e08faa144fd24f0b8b741e890" -dependencies = [ - "bitflags 2.10.0", - "com", - "libc", - "libloading", - "thiserror 1.0.69", - "widestring", - "winapi", -] - -[[package]] -name = "heck" -version = "0.4.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "95505c38b4572b2d910cecb0281560f54b440a19336cbbcb27bf6ce6adc6f5a8" - -[[package]] -name = "heck" -version = "0.5.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2304e00983f87ffb38b55b444b5e3b60a884b5d30c0fca7d82fe33449bbe55ea" - -[[package]] -name = "hermit-abi" -version = "0.3.9" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d231dfb89cfffdbc30e7fc41579ed6066ad03abda9e567ccafae602b97ec5024" - -[[package]] -name = "hermit-abi" -version = "0.5.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "fc0fef456e4baa96da950455cd02c081ca953b141298e41db3fc7e36b1da849c" - -[[package]] -name = "hex" -version = "0.4.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7f24254aa9a54b5c858eaee2f5bccdb46aaf0e486a595ed5fd8f86ba55232a70" - -[[package]] -name = "hexf-parse" -version = "0.2.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "dfa686283ad6dd069f105e5ab091b04c62850d3e4cf5d67debad1933f55023df" - -[[package]] -name = "i18n-config" -version = "0.4.8" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3e06b90c8a0d252e203c94344b21e35a30f3a3a85dc7db5af8f8df9f3e0c63ef" -dependencies = [ - "basic-toml", - "log", - "serde", - "serde_derive", - "thiserror 1.0.69", - "unic-langid", -] - -[[package]] -name = "i18n-embed" -version = "0.16.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a217bbb075dcaefb292efa78897fc0678245ca67f265d12c351e42268fcb0305" -dependencies = [ - "arc-swap", - "fluent", - "fluent-langneg", - "fluent-syntax", - "i18n-embed-impl", - "intl-memoizer", - "log", - "parking_lot 0.12.5", - "rust-embed", - "sys-locale", - "thiserror 1.0.69", - "unic-langid", - "walkdir", -] - -[[package]] -name = "i18n-embed-fl" -version = "0.10.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e598ed73b67db92f61e04672e599eef2991a262a40e1666735b8a86d2e7e9f30" -dependencies = [ - "find-crate", - "fluent", - "fluent-syntax", - "i18n-config", - "i18n-embed", - "proc-macro-error2", - "proc-macro2", - "quote", - "strsim", - "syn 2.0.114", - "unic-langid", -] - -[[package]] -name = "i18n-embed-impl" -version = "0.8.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0f2cc0e0523d1fe6fc2c6f66e5038624ea8091b3e7748b5e8e0c84b1698db6c2" -dependencies = [ - "find-crate", - "i18n-config", - "proc-macro2", - "quote", - "syn 2.0.114", -] - -[[package]] -name = "iana-time-zone" -version = "0.1.64" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "33e57f83510bb73707521ebaffa789ec8caf86f9657cad665b092b581d40e9fb" -dependencies = [ - "android_system_properties", - "core-foundation-sys", - "iana-time-zone-haiku", - "js-sys", - "log", - "wasm-bindgen", - "windows-core 0.62.2", -] - -[[package]] -name = "iana-time-zone-haiku" -version = "0.1.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f31827a206f56af32e590ba56d5d2d085f558508192593743f16b2306495269f" -dependencies = [ - "cc", -] - -[[package]] -name = "iced" -version = "0.14.0-dev" -source = "git+https://github.com/pop-os/libcosmic.git#b9c24d24212a865977db4871efc13ff890055648" -dependencies = [ - "dnd", - "iced_accessibility", - "iced_core", - "iced_futures", - "iced_renderer", - "iced_widget", - "iced_winit", - "image", - "mime", - "thiserror 1.0.69", - "window_clipboard", -] - -[[package]] -name = "iced_accessibility" -version = "0.1.0" -source = "git+https://github.com/pop-os/libcosmic.git#b9c24d24212a865977db4871efc13ff890055648" -dependencies = [ - "accesskit", - "accesskit_winit", -] - -[[package]] -name = "iced_core" -version = "0.14.0-dev" -source = "git+https://github.com/pop-os/libcosmic.git#b9c24d24212a865977db4871efc13ff890055648" -dependencies = [ - "bitflags 2.10.0", - "bytes", - "cosmic-client-toolkit", - "dnd", - "glam", - "iced_accessibility", - "log", - "mime", - "num-traits", - "once_cell", - "palette", - "raw-window-handle", - "rustc-hash 2.1.1", - "serde", - "smol_str", - "thiserror 1.0.69", - "web-time", - "window_clipboard", -] - -[[package]] -name = "iced_futures" -version = "0.14.0-dev" -source = "git+https://github.com/pop-os/libcosmic.git#b9c24d24212a865977db4871efc13ff890055648" -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 = "git+https://github.com/pop-os/glyphon.git?tag=iced-0.14-dev#6ef9d12a20cfd0f7bdf38136a26ded9f7459ec8b" -dependencies = [ - "cosmic-text", - "etagere", - "lru", - "rustc-hash 2.1.1", - "wgpu", -] - -[[package]] -name = "iced_graphics" -version = "0.14.0-dev" -source = "git+https://github.com/pop-os/libcosmic.git#b9c24d24212a865977db4871efc13ff890055648" -dependencies = [ - "bitflags 2.10.0", - "bytemuck", - "cosmic-text", - "half", - "iced_core", - "iced_futures", - "image", - "kamadak-exif", - "log", - "lyon_path", - "once_cell", - "raw-window-handle", - "rustc-hash 2.1.1", - "thiserror 1.0.69", - "unicode-segmentation", -] - -[[package]] -name = "iced_renderer" -version = "0.14.0-dev" -source = "git+https://github.com/pop-os/libcosmic.git#b9c24d24212a865977db4871efc13ff890055648" -dependencies = [ - "iced_graphics", - "iced_tiny_skia", - "iced_wgpu", - "log", - "thiserror 1.0.69", -] - -[[package]] -name = "iced_runtime" -version = "0.14.0-dev" -source = "git+https://github.com/pop-os/libcosmic.git#b9c24d24212a865977db4871efc13ff890055648" -dependencies = [ - "bytes", - "cosmic-client-toolkit", - "dnd", - "iced_accessibility", - "iced_core", - "iced_futures", - "raw-window-handle", - "thiserror 1.0.69", - "window_clipboard", -] - -[[package]] -name = "iced_tiny_skia" -version = "0.14.0-dev" -source = "git+https://github.com/pop-os/libcosmic.git#b9c24d24212a865977db4871efc13ff890055648" -dependencies = [ - "bytemuck", - "cosmic-text", - "iced_graphics", - "kurbo 0.10.4", - "log", - "resvg 0.42.0", - "rustc-hash 2.1.1", - "softbuffer", - "tiny-skia", -] - -[[package]] -name = "iced_wgpu" -version = "0.14.0-dev" -source = "git+https://github.com/pop-os/libcosmic.git#b9c24d24212a865977db4871efc13ff890055648" -dependencies = [ - "as-raw-xcb-connection", - "bitflags 2.10.0", - "bytemuck", - "cosmic-client-toolkit", - "futures", - "glam", - "guillotiere", - "iced_glyphon", - "iced_graphics", - "log", - "lyon", - "once_cell", - "raw-window-handle", - "resvg 0.42.0", - "rustc-hash 2.1.1", - "rustix 0.38.44", - "thiserror 1.0.69", - "tiny-xlib", - "wayland-backend", - "wayland-client", - "wayland-protocols", - "wayland-sys", - "wgpu", - "x11rb", -] - -[[package]] -name = "iced_widget" -version = "0.14.0-dev" -source = "git+https://github.com/pop-os/libcosmic.git#b9c24d24212a865977db4871efc13ff890055648" -dependencies = [ - "cosmic-client-toolkit", - "dnd", - "iced_accessibility", - "iced_renderer", - "iced_runtime", - "log", - "num-traits", - "once_cell", - "ouroboros", - "rustc-hash 2.1.1", - "thiserror 1.0.69", - "unicode-segmentation", - "window_clipboard", -] - -[[package]] -name = "iced_winit" -version = "0.14.0-dev" -source = "git+https://github.com/pop-os/libcosmic.git#b9c24d24212a865977db4871efc13ff890055648" -dependencies = [ - "cosmic-client-toolkit", - "dnd", - "iced_accessibility", - "iced_futures", - "iced_graphics", - "iced_runtime", - "log", - "raw-window-handle", - "rustc-hash 2.1.1", - "rustix 0.38.44", - "thiserror 1.0.69", - "tracing", - "wasm-bindgen-futures", - "wayland-backend", - "wayland-client", - "wayland-protocols", - "web-sys", - "winapi", - "window_clipboard", - "winit", - "xkbcommon 0.7.0", - "xkbcommon-dl", - "xkeysym", -] - -[[package]] -name = "icu_collections" -version = "2.1.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4c6b649701667bbe825c3b7e6388cb521c23d88644678e83c0c4d0a621a34b43" -dependencies = [ - "displaydoc", - "potential_utf", - "yoke", - "zerofrom", - "zerovec", -] - -[[package]] -name = "icu_locale_core" -version = "2.1.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "edba7861004dd3714265b4db54a3c390e880ab658fec5f7db895fae2046b5bb6" -dependencies = [ - "displaydoc", - "litemap", - "tinystr", - "writeable", - "zerovec", -] - -[[package]] -name = "icu_normalizer" -version = "2.1.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5f6c8828b67bf8908d82127b2054ea1b4427ff0230ee9141c54251934ab1b599" -dependencies = [ - "icu_collections", - "icu_normalizer_data", - "icu_properties", - "icu_provider", - "smallvec", - "zerovec", -] - -[[package]] -name = "icu_normalizer_data" -version = "2.1.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7aedcccd01fc5fe81e6b489c15b247b8b0690feb23304303a9e560f37efc560a" - -[[package]] -name = "icu_properties" -version = "2.1.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "020bfc02fe870ec3a66d93e677ccca0562506e5872c650f893269e08615d74ec" -dependencies = [ - "icu_collections", - "icu_locale_core", - "icu_properties_data", - "icu_provider", - "zerotrie", - "zerovec", -] - -[[package]] -name = "icu_properties_data" -version = "2.1.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "616c294cf8d725c6afcd8f55abc17c56464ef6211f9ed59cccffe534129c77af" - -[[package]] -name = "icu_provider" -version = "2.1.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "85962cf0ce02e1e0a629cc34e7ca3e373ce20dda4c4d7294bbd0bf1fdb59e614" -dependencies = [ - "displaydoc", - "icu_locale_core", - "writeable", - "yoke", - "zerofrom", - "zerotrie", - "zerovec", -] - -[[package]] -name = "ident_case" -version = "1.0.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b9e0384b61958566e926dc50660321d12159025e767c18e043daf26b70104c39" - -[[package]] -name = "idna" -version = "1.1.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3b0875f23caa03898994f6ddc501886a45c7d3d62d04d2d90788d47be1b1e4de" -dependencies = [ - "idna_adapter", - "smallvec", - "utf8_iter", -] - -[[package]] -name = "idna_adapter" -version = "1.2.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3acae9609540aa318d1bc588455225fb2085b9ed0c4f6bd0d9d5bcd86f1a0344" -dependencies = [ - "icu_normalizer", - "icu_properties", -] - -[[package]] -name = "image" -version = "0.25.9" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e6506c6c10786659413faa717ceebcb8f70731c0a60cbae39795fdf114519c1a" -dependencies = [ - "bytemuck", - "byteorder-lite", - "color_quant", - "exr", - "gif 0.14.1", - "image-webp", - "moxcms", - "num-traits", - "png 0.18.0", - "qoi", - "ravif", - "rayon", - "rgb", - "tiff", - "zune-core 0.5.0", - "zune-jpeg 0.5.8", -] - -[[package]] -name = "image-webp" -version = "0.2.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "525e9ff3e1a4be2fbea1fdf0e98686a6d98b4d8f937e1bf7402245af1909e8c3" -dependencies = [ - "byteorder-lite", - "quick-error", -] - -[[package]] -name = "imagesize" -version = "0.12.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "029d73f573d8e8d63e6d5020011d3255b28c3ba85d6cf870a07184ed23de9284" - -[[package]] -name = "imagesize" -version = "0.13.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "edcd27d72f2f071c64249075f42e205ff93c9a4c5f6c6da53e79ed9f9832c285" - -[[package]] -name = "imgref" -version = "1.12.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e7c5cedc30da3a610cac6b4ba17597bdf7152cf974e8aab3afb3d54455e371c8" - -[[package]] -name = "immutable-chunkmap" -version = "2.1.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9a3e98b1520e49e252237edc238a39869da9f3241f2ec19dc788c1d24694d1e4" -dependencies = [ - "arrayvec 0.7.6", -] - -[[package]] -name = "indexmap" -version = "2.13.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7714e70437a7dc3ac8eb7e6f8df75fd8eb422675fc7678aff7364301092b1017" -dependencies = [ - "equivalent", - "hashbrown 0.16.1", -] - -[[package]] -name = "inotify" -version = "0.11.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f37dccff2791ab604f9babef0ba14fbe0be30bd368dc541e2b08d07c8aa908f3" -dependencies = [ - "bitflags 2.10.0", - "inotify-sys", - "libc", -] - -[[package]] -name = "inotify-sys" -version = "0.1.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e05c02b5e89bff3b946cedeca278abc628fe811e604f027c45a8aa3cf793d0eb" -dependencies = [ - "libc", -] - -[[package]] -name = "instant" -version = "0.1.13" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e0242819d153cba4b4b05a5a8f2a7e9bbf97b6055b2a002b395c96b5ff3c0222" -dependencies = [ - "cfg-if", -] - -[[package]] -name = "interpolate_name" -version = "0.2.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c34819042dc3d3971c46c2190835914dfbe0c3c13f61449b2997f4e9722dfa60" -dependencies = [ - "proc-macro2", - "quote", - "syn 2.0.114", -] - -[[package]] -name = "intl-memoizer" -version = "0.5.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "310da2e345f5eb861e7a07ee182262e94975051db9e4223e909ba90f392f163f" -dependencies = [ - "type-map", - "unic-langid", -] - -[[package]] -name = "intl_pluralrules" -version = "7.0.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "078ea7b7c29a2b4df841a7f6ac8775ff6074020c6776d48491ce2268e068f972" -dependencies = [ - "unic-langid", -] - -[[package]] -name = "io-lifetimes" -version = "1.0.11" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "eae7b9aee968036d54dce06cebaefd919e4472e753296daccd6d344e3e2df0c2" -dependencies = [ - "hermit-abi 0.3.9", - "libc", - "windows-sys 0.48.0", -] - -[[package]] -name = "is-docker" -version = "0.2.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "928bae27f42bc99b60d9ac7334e3a21d10ad8f1835a4e12ec3ec0464765ed1b3" -dependencies = [ - "once_cell", -] - -[[package]] -name = "is-wsl" -version = "0.4.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "173609498df190136aa7dea1a91db051746d339e18476eed5ca40521f02d7aa5" -dependencies = [ - "is-docker", - "once_cell", -] - -[[package]] -name = "is_terminal_polyfill" -version = "1.70.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a6cb138bb79a146c1bd460005623e142ef0181e3d0219cb493e02f7d08a35695" - -[[package]] -name = "itertools" -version = "0.14.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2b192c782037fadd9cfa75548310488aabdbf3d2da73885b31bd0abd03351285" -dependencies = [ - "either", -] - -[[package]] -name = "itoa" -version = "1.0.17" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "92ecc6618181def0457392ccd0ee51198e065e016d1d527a7ac1b6dc7c1f09d2" - -[[package]] -name = "jiff" -version = "0.2.18" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e67e8da4c49d6d9909fe03361f9b620f58898859f5c7aded68351e85e71ecf50" -dependencies = [ - "jiff-static", - "log", - "portable-atomic", - "portable-atomic-util", - "serde_core", -] - -[[package]] -name = "jiff-static" -version = "0.2.18" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e0c84ee7f197eca9a86c6fd6cb771e55eb991632f15f2bc3ca6ec838929e6e78" -dependencies = [ - "proc-macro2", - "quote", - "syn 2.0.114", -] - -[[package]] -name = "jni" -version = "0.21.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1a87aa2bb7d2af34197c04845522473242e1aa17c12f4935d5856491a7fb8c97" -dependencies = [ - "cesu8", - "cfg-if", - "combine", - "jni-sys", - "log", - "thiserror 1.0.69", - "walkdir", - "windows-sys 0.45.0", -] - -[[package]] -name = "jni-sys" -version = "0.3.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8eaf4bc02d17cbdd7ff4c7438cafcdf7fb9a4613313ad11b4f8fefe7d3fa0130" - -[[package]] -name = "jobserver" -version = "0.1.34" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9afb3de4395d6b3e67a780b6de64b51c978ecf11cb9a462c66be7d4ca9039d33" -dependencies = [ - "getrandom 0.3.4", - "libc", -] - -[[package]] -name = "jpeg-decoder" -version = "0.3.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "00810f1d8b74be64b13dbf3db89ac67740615d6c891f0e7b6179326533011a07" - -[[package]] -name = "js-sys" -version = "0.3.83" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "464a3709c7f55f1f721e5389aa6ea4e3bc6aba669353300af094b29ffbdde1d8" -dependencies = [ - "once_cell", - "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" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6aae1df220ece3c0ada96b8153459b67eebe9ae9212258bb0134ae60416fdf76" -dependencies = [ - "libc", - "libloading", - "pkg-config", -] - -[[package]] -name = "khronos_api" -version = "3.1.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e2db585e1d738fc771bf08a151420d3ed193d9d895a36df7f6f8a9456b911ddc" - -[[package]] -name = "known-folders" -version = "1.4.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d463f34ca3c400fde3a054da0e0b8c6ffa21e4590922f3e18281bb5eeef4cbdc" -dependencies = [ - "windows-sys 0.61.2", -] - -[[package]] -name = "kqueue" -version = "1.1.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "eac30106d7dce88daf4a3fcb4879ea939476d5074a9b7ddd0fb97fa4bed5596a" -dependencies = [ - "kqueue-sys", - "libc", -] - -[[package]] -name = "kqueue-sys" -version = "1.0.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ed9625ffda8729b85e45cf04090035ac368927b8cebc34898e7c120f52e4838b" -dependencies = [ - "bitflags 1.3.2", - "libc", -] - -[[package]] -name = "kurbo" -version = "0.10.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1618d4ebd923e97d67e7cd363d80aef35fe961005cbbbb3d2dad8bdd1bc63440" -dependencies = [ - "arrayvec 0.7.6", - "smallvec", -] - -[[package]] -name = "kurbo" -version = "0.11.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c62026ae44756f8a599ba21140f350303d4f08dcdcc71b5ad9c9bb8128c13c62" -dependencies = [ - "arrayvec 0.7.6", - "euclid", - "smallvec", -] - -[[package]] -name = "lazy_static" -version = "1.5.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "bbd2bcb4c963f2ddae06a2efc7e9f3591312473c50c6685e1f298068316e66fe" - -[[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.180" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "bcc35a38544a891a5f7c865aca548a982ccb3b8650a5b06d0fd33a10283c56fc" - -[[package]] -name = "libcosmic" -version = "0.1.0" -source = "git+https://github.com/pop-os/libcosmic.git#b9c24d24212a865977db4871efc13ff890055648" -dependencies = [ - "apply", - "ashpd 0.12.0", - "auto_enums", - "chrono", - "cosmic-client-toolkit", - "cosmic-config", - "cosmic-freedesktop-icons", - "cosmic-settings-daemon", - "cosmic-theme", - "css-color", - "derive_setters", - "futures", - "i18n-embed", - "i18n-embed-fl", - "iced", - "iced_accessibility", - "iced_core", - "iced_futures", - "iced_renderer", - "iced_runtime", - "iced_tiny_skia", - "iced_wgpu", - "iced_widget", - "iced_winit", - "image", - "log", - "palette", - "phf 0.13.1", - "raw-window-handle", - "rfd", - "ron", - "rust-embed", - "serde", - "slotmap", - "taffy", - "thiserror 2.0.17", - "tokio", - "tracing", - "unicode-segmentation", - "url", - "zbus 5.13.0", -] - -[[package]] -name = "libfuzzer-sys" -version = "0.4.10" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5037190e1f70cbeef565bd267599242926f724d3b8a9f510fd7e0b540cfa4404" -dependencies = [ - "arbitrary", - "cc", -] - -[[package]] -name = "libloading" -version = "0.8.9" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d7c4b02199fee7c5d21a5ae7d8cfa79a6ef5bb2fc834d6e9058e89c825efdc55" -dependencies = [ - "cfg-if", - "windows-link", -] - -[[package]] -name = "libm" -version = "0.2.15" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f9fbbcab51052fe104eb5e5d351cf728d30a5be1fe14d9be8a3b097481fb97de" - -[[package]] -name = "libredox" -version = "0.1.12" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3d0b95e02c851351f877147b7deea7b1afb1df71b63aa5f8270716e0c5720616" -dependencies = [ - "bitflags 2.10.0", - "libc", - "redox_syscall 0.7.0", -] - -[[package]] -name = "linebender_resource_handle" -version = "0.1.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d4a5ff6bcca6c4867b1c4fd4ef63e4db7436ef363e0ad7531d1558856bae64f4" - -[[package]] -name = "linux-raw-sys" -version = "0.3.8" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ef53942eb7bf7ff43a617b3e2c1c4a5ecf5944a7c1bc12d7ee39bbb15e5c1519" - -[[package]] -name = "linux-raw-sys" -version = "0.4.15" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d26c52dbd32dccf2d10cac7725f8eae5296885fb5703b261f7d0a0739ec807ab" - -[[package]] -name = "linux-raw-sys" -version = "0.6.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2a385b1be4e5c3e362ad2ffa73c392e53f031eaa5b7d648e64cd87f27f6063d7" - -[[package]] -name = "linux-raw-sys" -version = "0.11.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "df1d3c3b53da64cf5760482273a98e575c651a67eec7f77df96b5b642de8f039" - -[[package]] -name = "litemap" -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" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "224399e74b87b5f3557511d98dff8b14089b3dadafcab6bb93eab67d3aace965" -dependencies = [ - "scopeguard", -] - -[[package]] -name = "log" -version = "0.4.29" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5e5032e24019045c762d3c0f28f5b6b8bbf38563a65908389bf7978758920897" - -[[package]] -name = "loop9" -version = "0.1.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0fae87c125b03c1d2c0150c90365d7d6bcc53fb73a9acaef207d2d065860f062" -dependencies = [ - "imgref", -] - -[[package]] -name = "lru" -version = "0.12.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "234cf4f4a04dc1f57e24b96cc0cd600cf2af460d4161ac5ecdd0af8e1f3b2a38" - -[[package]] -name = "lyon" -version = "1.0.16" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "dbcb7d54d54c8937364c9d41902d066656817dce1e03a44e5533afebd1ef4352" -dependencies = [ - "lyon_algorithms", - "lyon_tessellation", -] - -[[package]] -name = "lyon_algorithms" -version = "1.0.16" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f4c0829e28c4f336396f250d850c3987e16ce6db057ffe047ce0dd54aab6b647" -dependencies = [ - "lyon_path", - "num-traits", -] - -[[package]] -name = "lyon_geom" -version = "1.0.18" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e260b6de923e6e47adfedf6243013a7a874684165a6a277594ee3906021b2343" -dependencies = [ - "arrayvec 0.7.6", - "euclid", - "num-traits", -] - -[[package]] -name = "lyon_path" -version = "1.0.16" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1aeca86bcfd632a15984ba029b539ffb811e0a70bf55e814ef8b0f54f506fdeb" -dependencies = [ - "lyon_geom", - "num-traits", -] - -[[package]] -name = "lyon_tessellation" -version = "1.0.16" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f3f586142e1280335b1bc89539f7c97dd80f08fc43e9ab1b74ef0a42b04aa353" -dependencies = [ - "float_next_after", - "lyon_path", - "num-traits", -] - -[[package]] -name = "malloc_buf" -version = "0.0.6" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "62bb907fe88d54d8d9ce32a3cceab4218ed2f6b7d35617cafe9adf84e43919cb" -dependencies = [ - "libc", -] - -[[package]] -name = "maybe-rayon" -version = "0.1.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8ea1f30cedd69f0a2954655f7188c6a834246d2bcf1e315e2ac40c4b24dc9519" -dependencies = [ - "cfg-if", - "rayon", -] - -[[package]] -name = "memchr" -version = "2.7.6" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f52b00d39961fc5b2736ea853c9cc86238e165017a493d1d5c8eac6bdc4cc273" - -[[package]] -name = "memmap2" -version = "0.8.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "43a5a03cefb0d953ec0be133036f14e109412fa594edc2f77227249db66cc3ed" -dependencies = [ - "libc", -] - -[[package]] -name = "memmap2" -version = "0.9.9" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "744133e4a0e0a658e1374cf3bf8e415c4052a15a111acd372764c55b4177d490" -dependencies = [ - "libc", -] - -[[package]] -name = "memoffset" -version = "0.7.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5de893c32cde5f383baa4c04c5d6dbdd735cfd4a794b0debdb2bb1b421da5ff4" -dependencies = [ - "autocfg", -] - -[[package]] -name = "memoffset" -version = "0.9.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "488016bfae457b036d996092f6cb448677611ce4449e970ceaf42695203f218a" -dependencies = [ - "autocfg", -] - -[[package]] -name = "metal" -version = "0.29.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7ecfd3296f8c56b7c1f6fbac3c71cefa9d78ce009850c45000015f206dc7fa21" -dependencies = [ - "bitflags 2.10.0", - "block", - "core-graphics-types", - "foreign-types", - "log", - "objc", - "paste", -] - -[[package]] -name = "mime" -version = "0.1.0" -source = "git+https://github.com/pop-os/window_clipboard.git?tag=pop-0.13-2#6b9faab87bea9cebec6ae036906fd67fed254f5f" -dependencies = [ - "smithay-clipboard", -] - -[[package]] -name = "miniz_oxide" -version = "0.8.9" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1fa76a2c86f704bdb222d66965fb3d63269ce38518b83cb0575fca855ebb6316" -dependencies = [ - "adler2", - "simd-adler32", -] - -[[package]] -name = "mio" -version = "1.1.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a69bcab0ad47271a0234d9422b131806bf3968021e5dc9328caf2d4cd58557fc" -dependencies = [ - "libc", - "log", - "wasi 0.11.1+wasi-snapshot-preview1", - "windows-sys 0.61.2", -] - -[[package]] -name = "moxcms" -version = "0.7.11" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ac9557c559cd6fc9867e122e20d2cbefc9ca29d80d027a8e39310920ed2f0a97" -dependencies = [ - "num-traits", - "pxfm", -] - -[[package]] -name = "mutate_once" -version = "0.1.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "13d2233c9842d08cfe13f9eac96e207ca6a2ea10b80259ebe8ad0268be27d2af" - -[[package]] -name = "naga" -version = "22.1.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8bd5a652b6faf21496f2cfd88fc49989c8db0825d1f6746b1a71a6ede24a63ad" -dependencies = [ - "arrayvec 0.7.6", - "bit-set", - "bitflags 2.10.0", - "cfg_aliases 0.1.1", - "codespan-reporting", - "hexf-parse", - "indexmap", - "log", - "rustc-hash 1.1.0", - "spirv", - "termcolor", - "thiserror 1.0.69", - "unicode-xid", -] - -[[package]] -name = "ndk" -version = "0.9.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c3f42e7bbe13d351b6bead8286a43aac9534b82bd3cc43e47037f012ebfd62d4" -dependencies = [ - "bitflags 2.10.0", - "jni-sys", - "log", - "ndk-sys 0.6.0+11769913", - "num_enum", - "raw-window-handle", - "thiserror 1.0.69", -] - -[[package]] -name = "ndk-context" -version = "0.1.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "27b02d87554356db9e9a873add8782d4ea6e3e58ea071a9adb9a2e8ddb884a8b" - -[[package]] -name = "ndk-sys" -version = "0.5.0+25.2.9519653" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8c196769dd60fd4f363e11d948139556a344e79d451aeb2fa2fd040738ef7691" -dependencies = [ - "jni-sys", -] - -[[package]] -name = "ndk-sys" -version = "0.6.0+11769913" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ee6cda3051665f1fb8d9e08fc35c96d5a244fb1be711a03b71118828afc9a873" -dependencies = [ - "jni-sys", -] - -[[package]] -name = "new_debug_unreachable" -version = "1.0.6" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "650eef8c711430f1a879fdd01d4745a7deea475becfb90269c06775983bbf086" - -[[package]] -name = "nix" -version = "0.26.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "598beaf3cc6fdd9a5dfb1630c2800c7acd31df7aaf0f565796fba2b53ca1af1b" -dependencies = [ - "bitflags 1.3.2", - "cfg-if", - "libc", - "memoffset 0.7.1", -] - -[[package]] -name = "noctua" -version = "0.1.0" -dependencies = [ - "anyhow", - "cairo-rs", - "clap", - "dirs 5.0.1", - "env_logger", - "futures-util", - "i18n-embed", - "i18n-embed-fl", - "image", - "kamadak-exif", - "libcosmic", - "log", - "open", - "poppler", - "resvg 0.45.1", - "rust-embed", - "sha2", - "simple_logger", - "tokio", - "wallpaper", -] - -[[package]] -name = "nom" -version = "8.0.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "df9761775871bdef83bee530e60050f7e54b1105350d6884eb0fb4f46c2f9405" -dependencies = [ - "memchr", -] - -[[package]] -name = "noop_proc_macro" -version = "0.3.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0676bb32a98c1a483ce53e500a81ad9c3d5b3f7c920c28c24e9cb0980d0b5bc8" - -[[package]] -name = "notify" -version = "8.2.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4d3d07927151ff8575b7087f245456e549fea62edf0ec4e565a5ee50c8402bc3" -dependencies = [ - "bitflags 2.10.0", - "fsevent-sys", - "inotify", - "kqueue", - "libc", - "log", - "mio", - "notify-types", - "walkdir", - "windows-sys 0.60.2", -] - -[[package]] -name = "notify-types" -version = "2.0.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5e0826a989adedc2a244799e823aece04662b66609d96af8dff7ac6df9a8925d" - -[[package]] -name = "num-bigint" -version = "0.4.6" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a5e44f723f1133c9deac646763579fdb3ac745e418f2a7af9cd0c431da1f20b9" -dependencies = [ - "num-integer", - "num-traits", -] - -[[package]] -name = "num-conv" -version = "0.1.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "51d515d32fb182ee37cda2ccdcb92950d6a3c2893aa280e540671c2cd0f3b1d9" - -[[package]] -name = "num-derive" -version = "0.4.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ed3955f1a9c7c0c15e092f9c887db08b1fc683305fdf6eb6684f22555355e202" -dependencies = [ - "proc-macro2", - "quote", - "syn 2.0.114", -] - -[[package]] -name = "num-integer" -version = "0.1.46" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7969661fd2958a5cb096e56c8e1ad0444ac2bbcd0061bd28660485a44879858f" -dependencies = [ - "num-traits", -] - -[[package]] -name = "num-rational" -version = "0.4.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f83d14da390562dca69fc84082e73e548e1ad308d24accdedd2720017cb37824" -dependencies = [ - "num-bigint", - "num-integer", - "num-traits", -] - -[[package]] -name = "num-traits" -version = "0.2.19" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "071dfc062690e90b734c0b2273ce72ad0ffa95f0c74596bc250dcfd960262841" -dependencies = [ - "autocfg", - "libm", -] - -[[package]] -name = "num_cpus" -version = "1.17.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "91df4bbde75afed763b708b7eee1e8e7651e02d97f6d5dd763e89367e957b23b" -dependencies = [ - "hermit-abi 0.5.2", - "libc", -] - -[[package]] -name = "num_enum" -version = "0.7.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b1207a7e20ad57b847bbddc6776b968420d38292bbfe2089accff5e19e82454c" -dependencies = [ - "num_enum_derive", - "rustversion", -] - -[[package]] -name = "num_enum_derive" -version = "0.7.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ff32365de1b6743cb203b710788263c44a03de03802daf96092f2da4fe6ba4d7" -dependencies = [ - "proc-macro-crate 3.4.0", - "proc-macro2", - "quote", - "syn 2.0.114", -] - -[[package]] -name = "num_threads" -version = "0.1.7" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5c7398b9c8b70908f6371f47ed36737907c87c52af34c268fed0bf0ceb92ead9" -dependencies = [ - "libc", -] - -[[package]] -name = "objc" -version = "0.2.7" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "915b1b472bc21c53464d6c8461c9d3af805ba1ef837e1cac254428f4a77177b1" -dependencies = [ - "malloc_buf", -] - -[[package]] -name = "objc-foundation" -version = "0.1.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1add1b659e36c9607c7aab864a76c7a4c2760cd0cd2e120f3fb8b952c7e22bf9" -dependencies = [ - "block", - "objc", - "objc_id", -] - -[[package]] -name = "objc-sys" -version = "0.3.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "cdb91bdd390c7ce1a8607f35f3ca7151b65afc0ff5ff3b34fa350f7d7c7e4310" - -[[package]] -name = "objc2" -version = "0.5.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "46a785d4eeff09c14c487497c162e92766fbb3e4059a71840cecc03d9a50b804" -dependencies = [ - "objc-sys", - "objc2-encode", -] - -[[package]] -name = "objc2" -version = "0.6.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b7c2599ce0ec54857b29ce62166b0ed9b4f6f1a70ccc9a71165b6154caca8c05" -dependencies = [ - "objc2-encode", -] - -[[package]] -name = "objc2-app-kit" -version = "0.2.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e4e89ad9e3d7d297152b17d39ed92cd50ca8063a89a9fa569046d41568891eff" -dependencies = [ - "bitflags 2.10.0", - "block2 0.5.1", - "libc", - "objc2 0.5.2", - "objc2-core-data", - "objc2-core-image", - "objc2-foundation 0.2.2", - "objc2-quartz-core", -] - -[[package]] -name = "objc2-app-kit" -version = "0.3.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d49e936b501e5c5bf01fda3a9452ff86dc3ea98ad5f283e1455153142d97518c" -dependencies = [ - "bitflags 2.10.0", - "block2 0.6.2", - "objc2 0.6.3", - "objc2-foundation 0.3.2", -] - -[[package]] -name = "objc2-cloud-kit" -version = "0.2.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "74dd3b56391c7a0596a295029734d3c1c5e7e510a4cb30245f8221ccea96b009" -dependencies = [ - "bitflags 2.10.0", - "block2 0.5.1", - "objc2 0.5.2", - "objc2-core-location", - "objc2-foundation 0.2.2", -] - -[[package]] -name = "objc2-contacts" -version = "0.2.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a5ff520e9c33812fd374d8deecef01d4a840e7b41862d849513de77e44aa4889" -dependencies = [ - "block2 0.5.1", - "objc2 0.5.2", - "objc2-foundation 0.2.2", -] - -[[package]] -name = "objc2-core-data" -version = "0.2.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "617fbf49e071c178c0b24c080767db52958f716d9eabdf0890523aeae54773ef" -dependencies = [ - "bitflags 2.10.0", - "block2 0.5.1", - "objc2 0.5.2", - "objc2-foundation 0.2.2", -] - -[[package]] -name = "objc2-core-foundation" -version = "0.3.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2a180dd8642fa45cdb7dd721cd4c11b1cadd4929ce112ebd8b9f5803cc79d536" -dependencies = [ - "bitflags 2.10.0", - "dispatch2", - "objc2 0.6.3", -] - -[[package]] -name = "objc2-core-image" -version = "0.2.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "55260963a527c99f1819c4f8e3b47fe04f9650694ef348ffd2227e8196d34c80" -dependencies = [ - "block2 0.5.1", - "objc2 0.5.2", - "objc2-foundation 0.2.2", - "objc2-metal", -] - -[[package]] -name = "objc2-core-location" -version = "0.2.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "000cfee34e683244f284252ee206a27953279d370e309649dc3ee317b37e5781" -dependencies = [ - "block2 0.5.1", - "objc2 0.5.2", - "objc2-contacts", - "objc2-foundation 0.2.2", -] - -[[package]] -name = "objc2-encode" -version = "4.1.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ef25abbcd74fb2609453eb695bd2f860d389e457f67dc17cafc8b8cbc89d0c33" - -[[package]] -name = "objc2-foundation" -version = "0.2.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0ee638a5da3799329310ad4cfa62fbf045d5f56e3ef5ba4149e7452dcf89d5a8" -dependencies = [ - "bitflags 2.10.0", - "block2 0.5.1", - "dispatch", - "libc", - "objc2 0.5.2", -] - -[[package]] -name = "objc2-foundation" -version = "0.3.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e3e0adef53c21f888deb4fa59fc59f7eb17404926ee8a6f59f5df0fd7f9f3272" -dependencies = [ - "bitflags 2.10.0", - "objc2 0.6.3", - "objc2-core-foundation", -] - -[[package]] -name = "objc2-link-presentation" -version = "0.2.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a1a1ae721c5e35be65f01a03b6d2ac13a54cb4fa70d8a5da293d7b0020261398" -dependencies = [ - "block2 0.5.1", - "objc2 0.5.2", - "objc2-app-kit 0.2.2", - "objc2-foundation 0.2.2", -] - -[[package]] -name = "objc2-metal" -version = "0.2.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "dd0cba1276f6023976a406a14ffa85e1fdd19df6b0f737b063b95f6c8c7aadd6" -dependencies = [ - "bitflags 2.10.0", - "block2 0.5.1", - "objc2 0.5.2", - "objc2-foundation 0.2.2", -] - -[[package]] -name = "objc2-quartz-core" -version = "0.2.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e42bee7bff906b14b167da2bac5efe6b6a07e6f7c0a21a7308d40c960242dc7a" -dependencies = [ - "bitflags 2.10.0", - "block2 0.5.1", - "objc2 0.5.2", - "objc2-foundation 0.2.2", - "objc2-metal", -] - -[[package]] -name = "objc2-symbols" -version = "0.2.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0a684efe3dec1b305badae1a28f6555f6ddd3bb2c2267896782858d5a78404dc" -dependencies = [ - "objc2 0.5.2", - "objc2-foundation 0.2.2", -] - -[[package]] -name = "objc2-ui-kit" -version = "0.2.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b8bb46798b20cd6b91cbd113524c490f1686f4c4e8f49502431415f3512e2b6f" -dependencies = [ - "bitflags 2.10.0", - "block2 0.5.1", - "objc2 0.5.2", - "objc2-cloud-kit", - "objc2-core-data", - "objc2-core-image", - "objc2-core-location", - "objc2-foundation 0.2.2", - "objc2-link-presentation", - "objc2-quartz-core", - "objc2-symbols", - "objc2-uniform-type-identifiers", - "objc2-user-notifications", -] - -[[package]] -name = "objc2-uniform-type-identifiers" -version = "0.2.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "44fa5f9748dbfe1ca6c0b79ad20725a11eca7c2218bceb4b005cb1be26273bfe" -dependencies = [ - "block2 0.5.1", - "objc2 0.5.2", - "objc2-foundation 0.2.2", -] - -[[package]] -name = "objc2-user-notifications" -version = "0.2.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "76cfcbf642358e8689af64cee815d139339f3ed8ad05103ed5eaf73db8d84cb3" -dependencies = [ - "bitflags 2.10.0", - "block2 0.5.1", - "objc2 0.5.2", - "objc2-core-location", - "objc2-foundation 0.2.2", -] - -[[package]] -name = "objc_id" -version = "0.1.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c92d4ddb4bd7b50d730c215ff871754d0da6b2178849f8a2a2ab69712d0c073b" -dependencies = [ - "objc", -] - -[[package]] -name = "once_cell" -version = "1.21.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "42f5e15c9953c5e4ccceeb2e7382a716482c34515315f7b03532b8b4e8393d2d" - -[[package]] -name = "once_cell_polyfill" -version = "1.70.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "384b8ab6d37215f3c5301a95a4accb5d64aa607f1fcb26a11b5303878451b4fe" - -[[package]] -name = "open" -version = "5.3.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "43bb73a7fa3799b198970490a51174027ba0d4ec504b03cd08caf513d40024bc" -dependencies = [ - "is-wsl", - "libc", - "pathdiff", -] - -[[package]] -name = "option-ext" -version = "0.2.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "04744f49eae99ab78e0d5c0b603ab218f515ea8cfe5a456d7629ad883a3b6e7d" - -[[package]] -name = "orbclient" -version = "0.3.50" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "52ad2c6bae700b7aa5d1cc30c59bdd3a1c180b09dbaea51e2ae2b8e1cf211fdd" -dependencies = [ - "libc", - "libredox", -] - -[[package]] -name = "ordered-stream" -version = "0.2.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9aa2b01e1d916879f73a53d01d1d6cee68adbb31d6d9177a8cfce093cced1d50" -dependencies = [ - "futures-core", - "pin-project-lite", -] - -[[package]] -name = "ouroboros" -version = "0.18.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1e0f050db9c44b97a94723127e6be766ac5c340c48f2c4bb3ffa11713744be59" -dependencies = [ - "aliasable", - "ouroboros_macro", - "static_assertions", -] - -[[package]] -name = "ouroboros_macro" -version = "0.18.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3c7028bdd3d43083f6d8d4d5187680d0d3560d54df4cc9d752005268b41e64d0" -dependencies = [ - "heck 0.4.1", - "proc-macro2", - "proc-macro2-diagnostics", - "quote", - "syn 2.0.114", -] - -[[package]] -name = "owned_ttf_parser" -version = "0.25.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "36820e9051aca1014ddc75770aab4d68bc1e9e632f0f5627c4086bc216fb583b" -dependencies = [ - "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", - "fast-srgb8", - "palette_derive", - "phf 0.11.3", - "serde", -] - -[[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.114", -] - -[[package]] -name = "parking" -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" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "93857453250e3077bd71ff98b6a65ea6621a19bb0f559a85248955ac12c45a1a" -dependencies = [ - "lock_api", - "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]] -name = "parking_lot_core" -version = "0.9.12" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2621685985a2ebf1c516881c026032ac7deafcda1a2c9b7850dc81e3dfcb64c1" -dependencies = [ - "cfg-if", - "libc", - "redox_syscall 0.5.18", - "smallvec", - "windows-link", -] - -[[package]] -name = "paste" -version = "1.0.15" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "57c0d7b74b563b49d38dae00a0c37d4d6de9b432382b2892f0574ddcae73fd0a" - -[[package]] -name = "pastey" -version = "0.1.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "35fb2e5f958ec131621fdd531e9fc186ed768cbe395337403ae56c17a74c68ec" - -[[package]] -name = "pathdiff" -version = "0.2.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "df94ce210e5bc13cb6651479fa48d14f601d9858cfe0467f43ae157023b938d3" - -[[package]] -name = "percent-encoding" -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 0.11.3", - "phf_shared 0.11.3", -] - -[[package]] -name = "phf" -version = "0.13.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c1562dc717473dbaa4c1f85a36410e03c047b2e7df7f45ee938fbef64ae7fadf" -dependencies = [ - "phf_macros 0.13.1", - "phf_shared 0.13.1", - "serde", -] - -[[package]] -name = "phf_generator" -version = "0.11.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3c80231409c20246a13fddb31776fb942c38553c51e871f8cbd687a4cfb5843d" -dependencies = [ - "phf_shared 0.11.3", - "rand 0.8.5", -] - -[[package]] -name = "phf_generator" -version = "0.13.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "135ace3a761e564ec88c03a77317a7c6b80bb7f7135ef2544dbe054243b89737" -dependencies = [ - "fastrand 2.3.0", - "phf_shared 0.13.1", -] - -[[package]] -name = "phf_macros" -version = "0.11.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f84ac04429c13a7ff43785d75ad27569f2951ce0ffd30a3321230db2fc727216" -dependencies = [ - "phf_generator 0.11.3", - "phf_shared 0.11.3", - "proc-macro2", - "quote", - "syn 2.0.114", -] - -[[package]] -name = "phf_macros" -version = "0.13.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "812f032b54b1e759ccd5f8b6677695d5268c588701effba24601f6932f8269ef" -dependencies = [ - "phf_generator 0.13.1", - "phf_shared 0.13.1", - "proc-macro2", - "quote", - "syn 2.0.114", -] - -[[package]] -name = "phf_shared" -version = "0.11.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "67eabc2ef2a60eb7faa00097bd1ffdb5bd28e62bf39990626a582201b7a754e5" -dependencies = [ - "siphasher", -] - -[[package]] -name = "phf_shared" -version = "0.13.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e57fef6bc5981e38c2ce2d63bfa546861309f875b8a75f092d1d54ae2d64f266" -dependencies = [ - "siphasher", -] - -[[package]] -name = "pico-args" -version = "0.5.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5be167a7af36ee22fe3115051bc51f6e6c7054c9348e28deb4f49bd6f705a315" - -[[package]] -name = "pin-project" -version = "1.1.10" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "677f1add503faace112b9f1373e43e9e054bfdd22ff1a63c1bc485eaec6a6a8a" -dependencies = [ - "pin-project-internal", -] - -[[package]] -name = "pin-project-internal" -version = "1.1.10" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6e918e4ff8c4549eb882f14b3a4bc8c8bc93de829416eacf579f1207a8fbf861" -dependencies = [ - "proc-macro2", - "quote", - "syn 2.0.114", -] - -[[package]] -name = "pin-project-lite" -version = "0.2.16" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3b3cff922bd51709b605d9ead9aa71031d81447142d828eb4a6eba76fe619f9b" - -[[package]] -name = "pin-utils" -version = "0.1.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8b870d8c151b6f2fb93e84a13146138f05d02ed11c7e7c54f8826aaaf7c9f184" - -[[package]] -name = "piper" -version = "0.2.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "96c8c490f422ef9a4efd2cb5b42b76c8613d7e7dfc1caf667b8a3350a5acc066" -dependencies = [ - "atomic-waker", - "fastrand 2.3.0", - "futures-io", -] - -[[package]] -name = "pkg-config" -version = "0.3.32" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7edddbd0b52d732b21ad9a5fab5c704c14cd949e5e9a1ec5929a24fded1b904c" - -[[package]] -name = "png" -version = "0.17.16" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "82151a2fc869e011c153adc57cf2789ccb8d9906ce52c0b39a6b5697749d7526" -dependencies = [ - "bitflags 1.3.2", - "crc32fast", - "fdeflate", - "flate2", - "miniz_oxide", -] - -[[package]] -name = "png" -version = "0.18.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "97baced388464909d42d89643fe4361939af9b7ce7a31ee32a168f832a70f2a0" -dependencies = [ - "bitflags 2.10.0", - "crc32fast", - "fdeflate", - "flate2", - "miniz_oxide", -] - -[[package]] -name = "polling" -version = "2.8.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4b2d323e8ca7996b3e23126511a523f7e62924d93ecd5ae73b333815b0eb3dce" -dependencies = [ - "autocfg", - "bitflags 1.3.2", - "cfg-if", - "concurrent-queue", - "libc", - "log", - "pin-project-lite", - "windows-sys 0.48.0", -] - -[[package]] -name = "polling" -version = "3.11.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5d0e4f59085d47d8241c88ead0f274e8a0cb551f3625263c05eb8dd897c34218" -dependencies = [ - "cfg-if", - "concurrent-queue", - "hermit-abi 0.5.2", - "pin-project-lite", - "rustix 1.1.3", - "windows-sys 0.61.2", -] - -[[package]] -name = "pollster" -version = "0.4.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2f3a9f18d041e6d0e102a0a46750538147e5e8992d3b4873aaafee2520b00ce3" - -[[package]] -name = "poppler" -version = "0.4.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6580b718aba679b295299567119284d534d7bfc7510259d0c72273879831da8d" -dependencies = [ - "cairo-rs", - "glib", -] - -[[package]] -name = "portable-atomic" -version = "1.13.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f89776e4d69bb58bc6993e99ffa1d11f228b839984854c7daeb5d37f87cbe950" - -[[package]] -name = "portable-atomic-util" -version = "0.2.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d8a2f0d8d040d7848a709caf78912debcc3f33ee4b3cac47d73d1e1069e83507" -dependencies = [ - "portable-atomic", -] - -[[package]] -name = "potential_utf" -version = "0.1.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b73949432f5e2a09657003c25bca5e19a0e9c84f8058ca374f49e0ebe605af77" -dependencies = [ - "zerovec", -] - -[[package]] -name = "powerfmt" -version = "0.2.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "439ee305def115ba05938db6eb1644ff94165c5ab5e9420d1c1bcedbba909391" - -[[package]] -name = "ppv-lite86" -version = "0.2.21" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "85eae3c4ed2f50dcfe72643da4befc30deadb458a9b590d720cde2f2b1e97da9" -dependencies = [ - "zerocopy", -] - -[[package]] -name = "presser" -version = "0.3.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e8cf8e6a8aa66ce33f63993ffc4ea4271eb5b0530a9002db8455ea6050c77bfa" - -[[package]] -name = "proc-macro-crate" -version = "1.3.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7f4c021e1093a56626774e81216a4ce732a735e5bad4868a03f3ed65ca0c3919" -dependencies = [ - "once_cell", - "toml_edit 0.19.15", -] - -[[package]] -name = "proc-macro-crate" -version = "2.0.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b00f26d3400549137f92511a46ac1cd8ce37cb5598a96d382381458b992a5d24" -dependencies = [ - "toml_datetime 0.6.3", - "toml_edit 0.20.2", -] - -[[package]] -name = "proc-macro-crate" -version = "3.4.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "219cb19e96be00ab2e37d6e299658a0cfa83e52429179969b0f0121b4ac46983" -dependencies = [ - "toml_edit 0.23.10+spec-1.0.0", -] - -[[package]] -name = "proc-macro-error" -version = "1.0.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "da25490ff9892aab3fcf7c36f08cfb902dd3e71ca0f9f9517bea02a73a5ce38c" -dependencies = [ - "proc-macro-error-attr", - "proc-macro2", - "quote", - "syn 1.0.109", - "version_check", -] - -[[package]] -name = "proc-macro-error-attr" -version = "1.0.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a1be40180e52ecc98ad80b184934baf3d0d29f979574e439af5a55274b35f869" -dependencies = [ - "proc-macro2", - "quote", - "version_check", -] - -[[package]] -name = "proc-macro-error-attr2" -version = "2.0.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "96de42df36bb9bba5542fe9f1a054b8cc87e172759a1868aa05c1f3acc89dfc5" -dependencies = [ - "proc-macro2", - "quote", -] - -[[package]] -name = "proc-macro-error2" -version = "2.0.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "11ec05c52be0a07b08061f7dd003e7d7092e0472bc731b4af7bb1ef876109802" -dependencies = [ - "proc-macro-error-attr2", - "proc-macro2", - "quote", - "syn 2.0.114", -] - -[[package]] -name = "proc-macro2" -version = "1.0.105" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "535d180e0ecab6268a3e718bb9fd44db66bbbc256257165fc699dadf70d16fe7" -dependencies = [ - "unicode-ident", -] - -[[package]] -name = "proc-macro2-diagnostics" -version = "0.10.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "af066a9c399a26e020ada66a034357a868728e72cd426f3adcd35f80d88d88c8" -dependencies = [ - "proc-macro2", - "quote", - "syn 2.0.114", - "version_check", - "yansi", -] - -[[package]] -name = "profiling" -version = "1.0.17" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3eb8486b569e12e2c32ad3e204dbaba5e4b5b216e9367044f25f1dba42341773" -dependencies = [ - "profiling-procmacros", -] - -[[package]] -name = "profiling-procmacros" -version = "1.0.17" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "52717f9a02b6965224f95ca2a81e2e0c5c43baacd28ca057577988930b6c3d5b" -dependencies = [ - "quote", - "syn 2.0.114", -] - -[[package]] -name = "pxfm" -version = "0.1.27" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7186d3822593aa4393561d186d1393b3923e9d6163d3fbfd6e825e3e6cf3e6a8" -dependencies = [ - "num-traits", -] - -[[package]] -name = "qoi" -version = "0.4.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7f6d64c71eb498fe9eae14ce4ec935c555749aef511cca85b5568910d6e48001" -dependencies = [ - "bytemuck", -] - -[[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.38.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b66c2058c55a409d601666cffe35f04333cf1013010882cec174a7467cd4e21c" -dependencies = [ - "memchr", -] - -[[package]] -name = "quote" -version = "1.0.43" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "dc74d9a594b72ae6656596548f56f667211f8a97b3d4c3d467150794690dc40a" -dependencies = [ - "proc-macro2", -] - -[[package]] -name = "r-efi" -version = "5.3.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "69cdb34c158ceb288df11e18b4bd39de994f6657d83847bdffdbd7f346754b0f" - -[[package]] -name = "rand" -version = "0.8.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "34af8d1a0e25924bc5b7c43c079c942339d8f0a8b57c39049bef581b46327404" -dependencies = [ - "libc", - "rand_chacha 0.3.1", - "rand_core 0.6.4", -] - -[[package]] -name = "rand" -version = "0.9.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6db2770f06117d490610c7488547d543617b21bfa07796d7a12f6f1bd53850d1" -dependencies = [ - "rand_chacha 0.9.0", - "rand_core 0.9.3", -] - -[[package]] -name = "rand_chacha" -version = "0.3.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e6c10a63a0fa32252be49d21e7709d4d4baf8d231c2dbce1eaa8141b9b127d88" -dependencies = [ - "ppv-lite86", - "rand_core 0.6.4", -] - -[[package]] -name = "rand_chacha" -version = "0.9.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d3022b5f1df60f26e1ffddd6c66e8aa15de382ae63b3a0c1bfc0e4d3e3f325cb" -dependencies = [ - "ppv-lite86", - "rand_core 0.9.3", -] - -[[package]] -name = "rand_core" -version = "0.6.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ec0be4795e2f6a28069bec0b5ff3e2ac9bafc99e6a9a7dc3547996c5c816922c" -dependencies = [ - "getrandom 0.2.16", -] - -[[package]] -name = "rand_core" -version = "0.9.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "99d9a13982dcf210057a8a78572b2217b667c3beacbf3a0d8b454f6f82837d38" -dependencies = [ - "getrandom 0.3.4", -] - -[[package]] -name = "range-alloc" -version = "0.1.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c3d6831663a5098ea164f89cff59c6284e95f4e3c76ce9848d4529f5ccca9bde" - -[[package]] -name = "rangemap" -version = "1.7.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "973443cf09a9c8656b574a866ab68dfa19f0867d0340648c7d2f6a71b8a8ea68" - -[[package]] -name = "rav1e" -version = "0.8.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "43b6dd56e85d9483277cde964fd1bdb0428de4fec5ebba7540995639a21cb32b" -dependencies = [ - "aligned-vec", - "arbitrary", - "arg_enum_proc_macro", - "arrayvec 0.7.6", - "av-scenechange", - "av1-grain", - "bitstream-io", - "built", - "cfg-if", - "interpolate_name", - "itertools", - "libc", - "libfuzzer-sys", - "log", - "maybe-rayon", - "new_debug_unreachable", - "noop_proc_macro", - "num-derive", - "num-traits", - "paste", - "profiling", - "rand 0.9.2", - "rand_chacha 0.9.0", - "simd_helpers", - "thiserror 2.0.17", - "v_frame", - "wasm-bindgen", -] - -[[package]] -name = "ravif" -version = "0.12.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ef69c1990ceef18a116855938e74793a5f7496ee907562bd0857b6ac734ab285" -dependencies = [ - "avif-serialize", - "imgref", - "loop9", - "quick-error", - "rav1e", - "rayon", - "rgb", -] - -[[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.35.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6717cf23b488adf64b9d711329542ba34de147df262370221940dfabc2c91358" -dependencies = [ - "bytemuck", - "font-types", -] - -[[package]] -name = "read-fonts" -version = "0.36.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5eaa2941a4c05443ee3a7b26ab076a553c343ad5995230cc2b1d3e993bdc6345" -dependencies = [ - "bytemuck", - "core_maths", - "font-types", -] - -[[package]] -name = "redox_syscall" -version = "0.1.57" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "41cc0f7e4d5d4544e8861606a285bb08d3e70712ccc7d2b84d7c0ccfaf4b05ce" - -[[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.5.18" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ed2bf2547551a7053d6fdfafda3f938979645c44812fbfcda098faae3f1a362d" -dependencies = [ - "bitflags 2.10.0", -] - -[[package]] -name = "redox_syscall" -version = "0.7.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "49f3fe0889e69e2ae9e41f4d6c4c0181701d00e4697b356fb1f74173a5e0ee27" -dependencies = [ - "bitflags 2.10.0", -] - -[[package]] -name = "redox_users" -version = "0.3.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "de0737333e7a9502c789a36d7c7fa6092a49895d4faa31ca5df163857ded2e9d" -dependencies = [ - "getrandom 0.1.16", - "redox_syscall 0.1.57", - "rust-argon2", -] - -[[package]] -name = "redox_users" -version = "0.4.6" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ba009ff324d1fc1b900bd1fdb31564febe58a8ccc8a6fdbb93b543d33b13ca43" -dependencies = [ - "getrandom 0.2.16", - "libredox", - "thiserror 1.0.69", -] - -[[package]] -name = "redox_users" -version = "0.5.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a4e608c6638b9c18977b00b475ac1f28d14e84b27d8d42f70e0bf1e3dec127ac" -dependencies = [ - "getrandom 0.2.16", - "libredox", - "thiserror 2.0.17", -] - -[[package]] -name = "regex" -version = "1.12.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "843bc0191f75f3e22651ae5f1e72939ab2f72a4bc30fa80a066bd66edefc24d4" -dependencies = [ - "aho-corasick", - "memchr", - "regex-automata", - "regex-syntax", -] - -[[package]] -name = "regex-automata" -version = "0.4.13" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5276caf25ac86c8d810222b3dbb938e512c55c6831a10f3e6ed1c93b84041f1c" -dependencies = [ - "aho-corasick", - "memchr", - "regex-syntax", -] - -[[package]] -name = "regex-syntax" -version = "0.8.8" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7a2d987857b319362043e95f5353c0535c1f58eec5336fdfcf626430af7def58" - -[[package]] -name = "renderdoc-sys" -version = "1.1.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "19b30a45b0cd0bcca8037f3d0dc3421eaf95327a17cad11964fb8179b4fc4832" - -[[package]] -name = "resvg" -version = "0.42.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "944d052815156ac8fa77eaac055220e95ba0b01fa8887108ca710c03805d9051" -dependencies = [ - "gif 0.13.3", - "jpeg-decoder", - "log", - "pico-args", - "rgb", - "svgtypes", - "tiny-skia", - "usvg 0.42.0", -] - -[[package]] -name = "resvg" -version = "0.45.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a8928798c0a55e03c9ca6c4c6846f76377427d2c1e1f7e6de3c06ae57942df43" -dependencies = [ - "gif 0.13.3", - "image-webp", - "log", - "pico-args", - "rgb", - "svgtypes", - "tiny-skia", - "usvg 0.45.1", - "zune-jpeg 0.4.21", -] - -[[package]] -name = "rfd" -version = "0.15.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ef2bee61e6cffa4635c72d7d81a84294e28f0930db0ddcb0f66d10244674ebed" -dependencies = [ - "ashpd 0.11.0", - "block2 0.6.2", - "dispatch2", - "js-sys", - "log", - "objc2 0.6.3", - "objc2-app-kit 0.3.2", - "objc2-core-foundation", - "objc2-foundation 0.3.2", - "pollster", - "raw-window-handle", - "urlencoding", - "wasm-bindgen", - "wasm-bindgen-futures", - "web-sys", - "windows-sys 0.59.0", -] - -[[package]] -name = "rgb" -version = "0.8.52" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0c6a884d2998352bb4daf0183589aec883f16a6da1f4dde84d8e2e9a5409a1ce" -dependencies = [ - "bytemuck", -] - -[[package]] -name = "ron" -version = "0.11.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "db09040cc89e461f1a265139777a2bde7f8d8c67c4936f700c63ce3e2904d468" -dependencies = [ - "base64 0.22.1", - "bitflags 2.10.0", - "serde", - "serde_derive", - "unicode-ident", -] - -[[package]] -name = "roxmltree" -version = "0.20.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6c20b6793b5c2fa6553b250154b78d6d0db37e72700ae35fad9387a46f487c97" - -[[package]] -name = "rust-argon2" -version = "0.8.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4b18820d944b33caa75a71378964ac46f58517c92b6ae5f762636247c09e78fb" -dependencies = [ - "base64 0.13.1", - "blake2b_simd", - "constant_time_eq", - "crossbeam-utils", -] - -[[package]] -name = "rust-embed" -version = "8.9.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "947d7f3fad52b283d261c4c99a084937e2fe492248cb9a68a8435a861b8798ca" -dependencies = [ - "rust-embed-impl", - "rust-embed-utils", - "walkdir", -] - -[[package]] -name = "rust-embed-impl" -version = "8.9.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5fa2c8c9e8711e10f9c4fd2d64317ef13feaab820a4c51541f1a8c8e2e851ab2" -dependencies = [ - "proc-macro2", - "quote", - "rust-embed-utils", - "syn 2.0.114", - "walkdir", -] - -[[package]] -name = "rust-embed-utils" -version = "8.9.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "60b161f275cb337fe0a44d924a5f4df0ed69c2c39519858f931ce61c779d3475" -dependencies = [ - "sha2", - "walkdir", -] - -[[package]] -name = "rust-ini" -version = "0.12.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ac66e816614e124a692b6ac1b8437237a518c9155a3aacab83a373982630c715" - -[[package]] -name = "rustc-hash" -version = "1.1.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "08d43f7aa6b08d49f382cde6a7982047c3426db949b1424bc4b7ec9ae12c6ce2" - -[[package]] -name = "rustc-hash" -version = "2.1.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "357703d41365b4b27c590e3ed91eabb1b663f07c4c084095e60cbed4362dff0d" - -[[package]] -name = "rustix" -version = "0.37.28" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "519165d378b97752ca44bbe15047d5d3409e875f39327546b42ac81d7e18c1b6" -dependencies = [ - "bitflags 1.3.2", - "errno", - "io-lifetimes", - "libc", - "linux-raw-sys 0.3.8", - "windows-sys 0.48.0", -] - -[[package]] -name = "rustix" -version = "0.38.44" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "fdb5bc1ae2baa591800df16c9ca78619bf65c0488b41b96ccec5d11220d8c154" -dependencies = [ - "bitflags 2.10.0", - "errno", - "libc", - "linux-raw-sys 0.4.15", - "windows-sys 0.59.0", -] - -[[package]] -name = "rustix" -version = "1.1.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "146c9e247ccc180c1f61615433868c99f3de3ae256a30a43b49f67c2d9171f34" -dependencies = [ - "bitflags 2.10.0", - "errno", - "libc", - "linux-raw-sys 0.11.0", - "windows-sys 0.61.2", -] - -[[package]] -name = "rustversion" -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.10.0", - "bytemuck", - "smallvec", - "ttf-parser 0.21.1", - "unicode-bidi-mirroring 0.2.0", - "unicode-ccc 0.2.0", - "unicode-properties", - "unicode-script", -] - -[[package]] -name = "rustybuzz" -version = "0.20.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "fd3c7c96f8a08ee34eff8857b11b49b07d71d1c3f4e88f8a88d4c9e9f90b1702" -dependencies = [ - "bitflags 2.10.0", - "bytemuck", - "core_maths", - "log", - "smallvec", - "ttf-parser 0.25.1", - "unicode-bidi-mirroring 0.4.0", - "unicode-ccc 0.4.0", - "unicode-properties", - "unicode-script", -] - -[[package]] -name = "same-file" -version = "1.0.6" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "93fc1dc3aaa9bfed95e02e6eadabb4baf7e3078b0bd1b4d7b6b0b68378900502" -dependencies = [ - "winapi-util", -] - -[[package]] -name = "scoped-tls" -version = "1.0.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e1cf6437eb19a8f4a6cc0f7dca544973b0b78843adbfeb3683d1a94a0024a294" - -[[package]] -name = "scopeguard" -version = "1.2.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "94143f37725109f92c262ed2cf5e59bce7498c01bcc1502d7b9afe439a4e9f49" - -[[package]] -name = "sctk-adwaita" -version = "0.10.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b6277f0217056f77f1d8f49f2950ac6c278c0d607c45f5ee99328d792ede24ec" -dependencies = [ - "ab_glyph", - "log", - "memmap2 0.9.9", - "smithay-client-toolkit 0.19.2", - "tiny-skia", -] - -[[package]] -name = "self_cell" -version = "1.2.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b12e76d157a900eb52e81bc6e9f3069344290341720e9178cde2407113ac8d89" - -[[package]] -name = "serde" -version = "1.0.228" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9a8e94ea7f378bd32cbbd37198a4a91436180c5bb472411e48b5ec2e2124ae9e" -dependencies = [ - "serde_core", - "serde_derive", -] - -[[package]] -name = "serde_core" -version = "1.0.228" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "41d385c7d4ca58e59fc732af25c3983b67ac852c1a25000afe1175de458b67ad" -dependencies = [ - "serde_derive", -] - -[[package]] -name = "serde_derive" -version = "1.0.228" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d540f220d3187173da220f885ab66608367b6574e925011a9353e4badda91d79" -dependencies = [ - "proc-macro2", - "quote", - "syn 2.0.114", -] - -[[package]] -name = "serde_json" -version = "1.0.149" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "83fc039473c5595ace860d8c4fafa220ff474b3fc6bfdb4293327f1a37e94d86" -dependencies = [ - "indexmap", - "itoa", - "memchr", - "serde", - "serde_core", - "zmij", -] - -[[package]] -name = "serde_repr" -version = "0.1.20" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "175ee3e80ae9982737ca543e96133087cbd9a485eecc3bc4de9c1a37b47ea59c" -dependencies = [ - "proc-macro2", - "quote", - "syn 2.0.114", -] - -[[package]] -name = "serde_spanned" -version = "0.6.9" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "bf41e0cfaf7226dca15e8197172c295a782857fcb97fad1808a166870dee75a3" -dependencies = [ - "serde", -] - -[[package]] -name = "sha1" -version = "0.10.6" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e3bf829a2d51ab4a5ddf1352d8470c140cadc8301b2ae1789db023f01cedd6ba" -dependencies = [ - "cfg-if", - "cpufeatures", - "digest", -] - -[[package]] -name = "sha2" -version = "0.10.9" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a7507d819769d01a365ab707794a4084392c824f54a7a6a7862f8c3d0892b283" -dependencies = [ - "cfg-if", - "cpufeatures", - "digest", -] - -[[package]] -name = "shlex" -version = "1.3.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0fda2ff0d084019ba4d7c6f371c95d8fd75ce3524c3cb8fb653a3023f6323e64" - -[[package]] -name = "signal-hook-registry" -version = "1.4.8" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c4db69cba1110affc0e9f7bcd48bbf87b3f4fc7c61fc9155afd4c469eb3d6c1b" -dependencies = [ - "errno", - "libc", -] - -[[package]] -name = "simd-adler32" -version = "0.3.8" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e320a6c5ad31d271ad523dcf3ad13e2767ad8b1cb8f047f75a8aeaf8da139da2" - -[[package]] -name = "simd_helpers" -version = "0.1.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "95890f873bec569a0362c235787f3aca6e1e887302ba4840839bcc6459c42da6" -dependencies = [ - "quote", -] - -[[package]] -name = "simple_logger" -version = "4.3.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8e7e46c8c90251d47d08b28b8a419ffb4aede0f87c2eea95e17d1d5bacbf3ef1" -dependencies = [ - "colored", - "log", - "time", - "windows-sys 0.48.0", -] - -[[package]] -name = "simplecss" -version = "0.2.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7a9c6883ca9c3c7c90e888de77b7a5c849c779d25d74a1269b0218b14e8b136c" -dependencies = [ - "log", -] - -[[package]] -name = "siphasher" -version = "1.0.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "56199f7ddabf13fe5074ce809e7d3f42b42ae711800501b5b16ea82ad029c39d" - -[[package]] -name = "skrifa" -version = "0.37.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8c31071dedf532758ecf3fed987cdb4bd9509f900e026ab684b4ecb81ea49841" -dependencies = [ - "bytemuck", - "read-fonts 0.35.0", -] - -[[package]] -name = "skrifa" -version = "0.39.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9c9eb0b904a04d09bd68c65d946617b8ff733009999050f3b851c32fb3cfb60e" -dependencies = [ - "bytemuck", - "read-fonts 0.36.0", -] - -[[package]] -name = "slab" -version = "0.4.11" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7a2ae44ef20feb57a68b23d846850f861394c2e02dc425a50098ae8c90267589" - -[[package]] -name = "slotmap" -version = "1.1.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "bdd58c3c93c3d278ca835519292445cb4b0d4dc59ccfdf7ceadaab3f8aeb4038" -dependencies = [ - "version_check", -] - -[[package]] -name = "smallvec" -version = "1.15.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "67b1b7a3b5fe4f1376887184045fcf45c69e92af734b7aaddc05fb777b6fbd03" - -[[package]] -name = "smithay-client-toolkit" -version = "0.19.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3457dea1f0eb631b4034d61d4d8c32074caa6cd1ab2d59f2327bd8461e2c0016" -dependencies = [ - "bitflags 2.10.0", - "calloop 0.13.0", - "calloop-wayland-source 0.3.0", - "cursor-icon", - "libc", - "log", - "memmap2 0.9.9", - "rustix 0.38.44", - "thiserror 1.0.69", - "wayland-backend", - "wayland-client", - "wayland-csd-frame", - "wayland-cursor", - "wayland-protocols", - "wayland-protocols-wlr", - "wayland-scanner", - "xkeysym", -] - -[[package]] -name = "smithay-client-toolkit" -version = "0.20.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0512da38f5e2b31201a93524adb8d3136276fa4fe4aafab4e1f727a82b534cc0" -dependencies = [ - "bitflags 2.10.0", - "bytemuck", - "calloop 0.14.3", - "calloop-wayland-source 0.4.1", - "cursor-icon", - "libc", - "log", - "memmap2 0.9.9", - "pkg-config", - "rustix 1.1.3", - "thiserror 2.0.17", - "wayland-backend", - "wayland-client", - "wayland-csd-frame", - "wayland-cursor", - "wayland-protocols", - "wayland-protocols-experimental", - "wayland-protocols-misc", - "wayland-protocols-wlr", - "wayland-scanner", - "xkbcommon 0.8.0", - "xkeysym", -] - -[[package]] -name = "smithay-clipboard" -version = "0.8.0" -source = "git+https://github.com/pop-os/smithay-clipboard?tag=pop-dnd-5#5a3007def49eb678d1144850c9ee04b80707c56a" -dependencies = [ - "libc", - "raw-window-handle", - "smithay-client-toolkit 0.19.2", - "wayland-backend", -] - -[[package]] -name = "smol_str" -version = "0.2.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "dd538fb6910ac1099850255cf94a94df6551fbdd602454387d0adb2d1ca6dead" -dependencies = [ - "serde", -] - -[[package]] -name = "socket2" -version = "0.4.10" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9f7916fc008ca5542385b89a3d3ce689953c143e9304a9bf8beec1de48994c0d" -dependencies = [ - "libc", - "winapi", -] - -[[package]] -name = "socket2" -version = "0.6.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "17129e116933cf371d018bb80ae557e889637989d8638274fb25622827b03881" -dependencies = [ - "libc", - "windows-sys 0.60.2", -] - -[[package]] -name = "softbuffer" -version = "0.4.1" -source = "git+https://github.com/pop-os/softbuffer?tag=cosmic-4.0#a3f77e251e7422803f693df6e3fc313c010c4dcb" -dependencies = [ - "as-raw-xcb-connection", - "bytemuck", - "cfg_aliases 0.2.1", - "cocoa", - "core-graphics", - "drm", - "fastrand 2.3.0", - "foreign-types", - "js-sys", - "log", - "memmap2 0.9.9", - "objc", - "raw-window-handle", - "redox_syscall 0.5.18", - "rustix 0.38.44", - "tiny-xlib", - "wasm-bindgen", - "wayland-backend", - "wayland-client", - "wayland-sys", - "web-sys", - "windows-sys 0.52.0", - "x11rb", -] - -[[package]] -name = "spirv" -version = "0.3.0+sdk-1.3.268.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "eda41003dc44290527a59b13432d4a0379379fa074b70174882adfbdfd917844" -dependencies = [ - "bitflags 2.10.0", -] - -[[package]] -name = "stable_deref_trait" -version = "1.2.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6ce2be8dc25455e1f91df71bfa12ad37d7af1092ae736f3a6cd0e37bc7810596" - -[[package]] -name = "static_assertions" -version = "1.1.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a2eb9349b6444b326872e140eb1cf5e7c522154d69e7a0ffb0fb81c06b37543f" - -[[package]] -name = "strict-num" -version = "0.1.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6637bab7722d379c8b41ba849228d680cc12d0a45ba1fa2b48f2a30577a06731" -dependencies = [ - "float-cmp", -] - -[[package]] -name = "strsim" -version = "0.11.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7da8b5736845d9f2fcb837ea5d9e2628564b3b043a70948a3f0b778838c5fb4f" - -[[package]] -name = "svg_fmt" -version = "0.4.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0193cc4331cfd2f3d2011ef287590868599a2f33c3e69bc22c1a3d3acf9e02fb" - -[[package]] -name = "svgtypes" -version = "0.15.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "68c7541fff44b35860c1a7a47a7cadf3e4a304c457b58f9870d9706ece028afc" -dependencies = [ - "kurbo 0.11.3", - "siphasher", -] - -[[package]] -name = "swash" -version = "0.2.6" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "47846491253e976bdd07d0f9cc24b7daf24720d11309302ccbbc6e6b6e53550a" -dependencies = [ - "skrifa 0.37.0", - "yazi", - "zeno", -] - -[[package]] -name = "syn" -version = "1.0.109" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "72b64191b275b66ffe2469e8af2c1cfe3bafa67b529ead792a6d0160888b4237" -dependencies = [ - "proc-macro2", - "quote", - "unicode-ident", -] - -[[package]] -name = "syn" -version = "2.0.114" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d4d107df263a3013ef9b1879b0df87d706ff80f65a86ea879bd9c31f9b307c2a" -dependencies = [ - "proc-macro2", - "quote", - "unicode-ident", -] - -[[package]] -name = "synstructure" -version = "0.13.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "728a70f3dbaf5bab7f0c4b1ac8d7ae5ea60a4b5549c8a5914361c99147a709d2" -dependencies = [ - "proc-macro2", - "quote", - "syn 2.0.114", -] - -[[package]] -name = "sys-locale" -version = "0.3.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8eab9a99a024a169fe8a903cf9d4a3b3601109bcc13bd9e3c6fff259138626c4" -dependencies = [ - "libc", -] - -[[package]] -name = "system-deps" -version = "6.2.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a3e535eb8dded36d55ec13eddacd30dec501792ff23a0b1682c38601b8cf2349" -dependencies = [ - "cfg-expr", - "heck 0.5.0", - "pkg-config", - "toml 0.8.2", - "version-compare", -] - -[[package]] -name = "taffy" -version = "0.9.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "41ba83ebaf2954d31d05d67340fd46cebe99da2b7133b0dd68d70c65473a437b" -dependencies = [ - "arrayvec 0.7.6", - "grid", - "serde", - "slotmap", -] - -[[package]] -name = "target-lexicon" -version = "0.12.16" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "61c41af27dd6d1e27b1b16b489db798443478cef1f06a660c96db617ba5de3b1" - -[[package]] -name = "tempfile" -version = "3.24.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "655da9c7eb6305c55742045d5a8d2037996d61d8de95806335c7c86ce0f82e9c" -dependencies = [ - "fastrand 2.3.0", - "getrandom 0.3.4", - "once_cell", - "rustix 1.1.3", - "windows-sys 0.61.2", -] - -[[package]] -name = "termcolor" -version = "1.4.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "06794f8f6c5c898b3275aebefa6b8a1cb24cd2c6c79397ab15774837a0bc5755" -dependencies = [ - "winapi-util", -] - -[[package]] -name = "thiserror" -version = "1.0.69" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b6aaf5339b578ea85b50e080feb250a3e8ae8cfcdff9a461c9ec2904bc923f52" -dependencies = [ - "thiserror-impl 1.0.69", -] - -[[package]] -name = "thiserror" -version = "2.0.17" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f63587ca0f12b72a0600bcba1d40081f830876000bb46dd2337a3051618f4fc8" -dependencies = [ - "thiserror-impl 2.0.17", -] - -[[package]] -name = "thiserror-impl" -version = "1.0.69" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4fee6c4efc90059e10f81e6d42c60a18f76588c3d74cb83a0b242a2b6c7504c1" -dependencies = [ - "proc-macro2", - "quote", - "syn 2.0.114", -] - -[[package]] -name = "thiserror-impl" -version = "2.0.17" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3ff15c8ecd7de3849db632e14d18d2571fa09dfc5ed93479bc4485c7a517c913" -dependencies = [ - "proc-macro2", - "quote", - "syn 2.0.114", -] - -[[package]] -name = "tiff" -version = "0.10.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "af9605de7fee8d9551863fd692cce7637f548dbd9db9180fcc07ccc6d26c336f" -dependencies = [ - "fax", - "flate2", - "half", - "quick-error", - "weezl", - "zune-jpeg 0.4.21", -] - -[[package]] -name = "time" -version = "0.3.44" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "91e7d9e3bb61134e77bde20dd4825b97c010155709965fedf0f49bb138e52a9d" -dependencies = [ - "deranged", - "itoa", - "libc", - "num-conv", - "num_threads", - "powerfmt", - "serde", - "time-core", - "time-macros", -] - -[[package]] -name = "time-core" -version = "0.1.6" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "40868e7c1d2f0b8d73e4a8c7f0ff63af4f6d19be117e90bd73eb1d62cf831c6b" - -[[package]] -name = "time-macros" -version = "0.2.24" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "30cfb0125f12d9c277f35663a0a33f8c30190f4e4574868a330595412d34ebf3" -dependencies = [ - "num-conv", - "time-core", -] - -[[package]] -name = "tiny-skia" -version = "0.11.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "83d13394d44dae3207b52a326c0c85a8bf87f1541f23b0d143811088497b09ab" -dependencies = [ - "arrayref", - "arrayvec 0.7.6", - "bytemuck", - "cfg-if", - "log", - "png 0.17.16", - "tiny-skia-path", -] - -[[package]] -name = "tiny-skia-path" -version = "0.11.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9c9e7fc0c2e86a30b117d0462aa261b72b7a99b7ebd7deb3a14ceda95c5bdc93" -dependencies = [ - "arrayref", - "bytemuck", - "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", - "pkg-config", - "tracing", -] - -[[package]] -name = "tinystr" -version = "0.8.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "42d3e9c45c09de15d06dd8acf5f4e0e399e85927b7f00711024eb7ae10fa4869" -dependencies = [ - "displaydoc", - "serde_core", - "zerovec", -] - -[[package]] -name = "tinyvec" -version = "1.10.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "bfa5fdc3bce6191a1dbc8c02d5c8bffcf557bafa17c124c5264a458f1b0613fa" -dependencies = [ - "tinyvec_macros", -] - -[[package]] -name = "tinyvec_macros" -version = "0.1.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1f3ccbac311fea05f86f61904b462b55fb3df8837a366dfc601a0161d0532f20" - -[[package]] -name = "tokio" -version = "1.49.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "72a2903cd7736441aac9df9d7688bd0ce48edccaadf181c3b90be801e81d3d86" -dependencies = [ - "bytes", - "libc", - "mio", - "parking_lot 0.12.5", - "pin-project-lite", - "signal-hook-registry", - "socket2 0.6.1", - "tokio-macros", - "tracing", - "windows-sys 0.61.2", -] - -[[package]] -name = "tokio-macros" -version = "2.6.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "af407857209536a95c8e56f8231ef2c2e2aff839b22e07a1ffcbc617e9db9fa5" -dependencies = [ - "proc-macro2", - "quote", - "syn 2.0.114", -] - -[[package]] -name = "tokio-stream" -version = "0.1.18" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "32da49809aab5c3bc678af03902d4ccddea2a87d028d86392a4b1560c6906c70" -dependencies = [ - "futures-core", - "pin-project-lite", - "tokio", -] - -[[package]] -name = "toml" -version = "0.5.11" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f4f7f0dd8d50a853a531c426359045b1998f04219d88799810762cd4ad314234" -dependencies = [ - "serde", -] - -[[package]] -name = "toml" -version = "0.8.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "185d8ab0dfbb35cf1399a6344d8484209c088f75f8f68230da55d48d95d43e3d" -dependencies = [ - "serde", - "serde_spanned", - "toml_datetime 0.6.3", - "toml_edit 0.20.2", -] - -[[package]] -name = "toml_datetime" -version = "0.6.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7cda73e2f1397b1262d6dfdcef8aafae14d1de7748d66822d3bfeeb6d03e5e4b" -dependencies = [ - "serde", -] - -[[package]] -name = "toml_datetime" -version = "0.7.5+spec-1.1.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "92e1cfed4a3038bc5a127e35a2d360f145e1f4b971b551a2ba5fd7aedf7e1347" -dependencies = [ - "serde_core", -] - -[[package]] -name = "toml_edit" -version = "0.19.15" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1b5bb770da30e5cbfde35a2d7b9b8a2c4b8ef89548a7a6aeab5c9a576e3e7421" -dependencies = [ - "indexmap", - "toml_datetime 0.6.3", - "winnow 0.5.40", -] - -[[package]] -name = "toml_edit" -version = "0.20.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "396e4d48bbb2b7554c944bde63101b5ae446cff6ec4a24227428f15eb72ef338" -dependencies = [ - "indexmap", - "serde", - "serde_spanned", - "toml_datetime 0.6.3", - "winnow 0.5.40", -] - -[[package]] -name = "toml_edit" -version = "0.23.10+spec-1.0.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "84c8b9f757e028cee9fa244aea147aab2a9ec09d5325a9b01e0a49730c2b5269" -dependencies = [ - "indexmap", - "toml_datetime 0.7.5+spec-1.1.0", - "toml_parser", - "winnow 0.7.14", -] - -[[package]] -name = "toml_parser" -version = "1.0.6+spec-1.1.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a3198b4b0a8e11f09dd03e133c0280504d0801269e9afa46362ffde1cbeebf44" -dependencies = [ - "winnow 0.7.14", -] - -[[package]] -name = "tracing" -version = "0.1.44" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "63e71662fa4b2a2c3a26f570f037eb95bb1f85397f3cd8076caed2f026a6d100" -dependencies = [ - "log", - "pin-project-lite", - "tracing-attributes", - "tracing-core", -] - -[[package]] -name = "tracing-attributes" -version = "0.1.31" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7490cfa5ec963746568740651ac6781f701c9c5ea257c58e057f3ba8cf69e8da" -dependencies = [ - "proc-macro2", - "quote", - "syn 2.0.114", -] - -[[package]] -name = "tracing-core" -version = "0.1.36" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "db97caf9d906fbde555dd62fa95ddba9eecfd14cb388e4f491a66d74cd5fb79a" -dependencies = [ - "once_cell", -] - -[[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" -dependencies = [ - "core_maths", -] - -[[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" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "562d481066bde0658276a35467c4af00bdc6ee726305698a55b86e61d7ad82bb" - -[[package]] -name = "uds_windows" -version = "1.1.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "89daebc3e6fd160ac4aa9fc8b3bf71e1f74fbf92367ae71fb83a037e8bf164b9" -dependencies = [ - "memoffset 0.9.1", - "tempfile", - "winapi", -] - -[[package]] -name = "unic-langid" -version = "0.9.6" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a28ba52c9b05311f4f6e62d5d9d46f094bd6e84cb8df7b3ef952748d752a7d05" -dependencies = [ - "unic-langid-impl", -] - -[[package]] -name = "unic-langid-impl" -version = "0.9.6" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "dce1bf08044d4b7a94028c93786f8566047edc11110595914de93362559bc658" -dependencies = [ - "serde", - "tinystr", -] - -[[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-bidi-mirroring" -version = "0.4.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5dfa6e8c60bb66d49db113e0125ee8711b7647b5579dc7f5f19c42357ed039fe" - -[[package]] -name = "unicode-ccc" -version = "0.2.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1df77b101bcc4ea3d78dafc5ad7e4f58ceffe0b2b16bf446aeb50b6cb4157656" - -[[package]] -name = "unicode-ccc" -version = "0.4.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ce61d488bcdc9bc8b5d1772c404828b17fc481c0a582b5581e95fb233aef503e" - -[[package]] -name = "unicode-ident" -version = "1.0.22" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9312f7c4f6ff9069b165498234ce8be658059c6728633667c526e27dc2cf1df5" - -[[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" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f6ccf251212114b54433ec949fd6a7841275f9ada20dddd2f29e9ceea4501493" - -[[package]] -name = "unicode-vo" -version = "0.1.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b1d386ff53b415b7fe27b50bb44679e2cc4660272694b7b6f3326d8480823a94" - -[[package]] -name = "unicode-width" -version = "0.1.14" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7dd6e30e90baa6f72411720665d41d89b9a3d039dc45b8faea1ddd07f617f6af" - -[[package]] -name = "unicode-xid" -version = "0.2.6" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ebc1c04c71510c7f702b52b7c350734c9ff1295c464a03335b00bb84fc54f853" - -[[package]] -name = "url" -version = "2.5.8" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ff67a8a4397373c3ef660812acab3268222035010ab8680ec4215f38ba3d0eed" -dependencies = [ - "form_urlencoded", - "idna", - "percent-encoding", - "serde", - "serde_derive", -] - -[[package]] -name = "urlencoding" -version = "2.1.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "daf8dba3b7eb870caf1ddeed7bc9d2a049f3cfdfae7cb521b087cc33ae4c49da" - -[[package]] -name = "usvg" -version = "0.42.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b84ea542ae85c715f07b082438a4231c3760539d902e11d093847a0b22963032" -dependencies = [ - "base64 0.22.1", - "data-url", - "flate2", - "fontdb 0.18.0", - "imagesize 0.12.0", - "kurbo 0.11.3", - "log", - "pico-args", - "roxmltree", - "rustybuzz 0.14.1", - "simplecss", - "siphasher", - "strict-num", - "svgtypes", - "tiny-skia-path", - "unicode-bidi", - "unicode-script", - "unicode-vo", - "xmlwriter", -] - -[[package]] -name = "usvg" -version = "0.45.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "80be9b06fbae3b8b303400ab20778c80bbaf338f563afe567cf3c9eea17b47ef" -dependencies = [ - "base64 0.22.1", - "data-url", - "flate2", - "fontdb 0.23.0", - "imagesize 0.13.0", - "kurbo 0.11.3", - "log", - "pico-args", - "roxmltree", - "rustybuzz 0.20.1", - "simplecss", - "siphasher", - "strict-num", - "svgtypes", - "tiny-skia-path", - "unicode-bidi", - "unicode-script", - "unicode-vo", - "xmlwriter", -] - -[[package]] -name = "utf8_iter" -version = "1.0.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b6c140620e7ffbb22c2dee59cafe6084a59b5ffc27a8859a5f0d494b5d52b6be" - -[[package]] -name = "utf8parse" -version = "0.2.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "06abde3611657adf66d383f00b093d7faecc7fa57071cce2578660c9f1010821" - -[[package]] -name = "uuid" -version = "1.19.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e2e054861b4bd027cd373e18e8d8d8e6548085000e41290d95ce0c373a654b4a" -dependencies = [ - "js-sys", - "serde_core", - "wasm-bindgen", -] - -[[package]] -name = "v_frame" -version = "0.3.9" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "666b7727c8875d6ab5db9533418d7c764233ac9c0cff1d469aec8fa127597be2" -dependencies = [ - "aligned-vec", - "num-traits", - "wasm-bindgen", -] - -[[package]] -name = "version-compare" -version = "0.2.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "03c2856837ef78f57382f06b2b8563a2f512f7185d732608fd9176cb3b8edf0e" - -[[package]] -name = "version_check" -version = "0.9.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0b928f33d975fc6ad9f86c8f283853ad26bdd5b10b7f1542aa2fa15e2289105a" - -[[package]] -name = "waker-fn" -version = "1.2.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "317211a0dc0ceedd78fb2ca9a44aed3d7b9b26f81870d485c07122b4350673b7" - -[[package]] -name = "walkdir" -version = "2.5.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "29790946404f91d9c5d06f9874efddea1dc06c5efe94541a7d6863108e3a5e4b" -dependencies = [ - "same-file", - "winapi-util", -] - -[[package]] -name = "wallpaper" -version = "3.2.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0547c84bf49b1096b20ce49736b86cd27f8225fc426665d3fba19e71e44c4d46" -dependencies = [ - "dirs 1.0.5", - "enquote", - "rust-ini", - "winapi", - "winreg", -] - -[[package]] -name = "wasi" -version = "0.9.0+wasi-snapshot-preview1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "cccddf32554fecc6acb585f82a32a72e28b48f8c4c1883ddfeeeaa96f7d8e519" - -[[package]] -name = "wasi" -version = "0.11.1+wasi-snapshot-preview1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ccf3ec651a847eb01de73ccad15eb7d99f80485de043efb2f370cd654f4ea44b" - -[[package]] -name = "wasip2" -version = "1.0.1+wasi-0.2.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0562428422c63773dad2c345a1882263bbf4d65cf3f42e90921f787ef5ad58e7" -dependencies = [ - "wit-bindgen", -] - -[[package]] -name = "wasm-bindgen" -version = "0.2.106" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0d759f433fa64a2d763d1340820e46e111a7a5ab75f993d1852d70b03dbb80fd" -dependencies = [ - "cfg-if", - "once_cell", - "rustversion", - "wasm-bindgen-macro", - "wasm-bindgen-shared", -] - -[[package]] -name = "wasm-bindgen-futures" -version = "0.4.56" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "836d9622d604feee9e5de25ac10e3ea5f2d65b41eac0d9ce72eb5deae707ce7c" -dependencies = [ - "cfg-if", - "js-sys", - "once_cell", - "wasm-bindgen", - "web-sys", -] - -[[package]] -name = "wasm-bindgen-macro" -version = "0.2.106" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "48cb0d2638f8baedbc542ed444afc0644a29166f1595371af4fecf8ce1e7eeb3" -dependencies = [ - "quote", - "wasm-bindgen-macro-support", -] - -[[package]] -name = "wasm-bindgen-macro-support" -version = "0.2.106" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "cefb59d5cd5f92d9dcf80e4683949f15ca4b511f4ac0a6e14d4e1ac60c6ecd40" -dependencies = [ - "bumpalo", - "proc-macro2", - "quote", - "syn 2.0.114", - "wasm-bindgen-shared", -] - -[[package]] -name = "wasm-bindgen-shared" -version = "0.2.106" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "cbc538057e648b67f72a982e708d485b2efa771e1ac05fec311f9f63e5800db4" -dependencies = [ - "unicode-ident", -] - -[[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 = "wayland-backend" -version = "0.3.12" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "fee64194ccd96bf648f42a65a7e589547096dfa702f7cadef84347b66ad164f9" -dependencies = [ - "cc", - "downcast-rs", - "rustix 1.1.3", - "scoped-tls", - "smallvec", - "wayland-sys", -] - -[[package]] -name = "wayland-client" -version = "0.31.12" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b8e6faa537fbb6c186cb9f1d41f2f811a4120d1b57ec61f50da451a0c5122bec" -dependencies = [ - "bitflags 2.10.0", - "rustix 1.1.3", - "wayland-backend", - "wayland-scanner", -] - -[[package]] -name = "wayland-csd-frame" -version = "0.3.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "625c5029dbd43d25e6aa9615e88b829a5cad13b2819c4ae129fdbb7c31ab4c7e" -dependencies = [ - "bitflags 2.10.0", - "cursor-icon", - "wayland-backend", -] - -[[package]] -name = "wayland-cursor" -version = "0.31.12" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5864c4b5b6064b06b1e8b74ead4a98a6c45a285fe7a0e784d24735f011fdb078" -dependencies = [ - "rustix 1.1.3", - "wayland-client", - "xcursor", -] - -[[package]] -name = "wayland-protocols" -version = "0.32.10" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "baeda9ffbcfc8cd6ddaade385eaf2393bd2115a69523c735f12242353c3df4f3" -dependencies = [ - "bitflags 2.10.0", - "wayland-backend", - "wayland-client", - "wayland-scanner", - "wayland-server", -] - -[[package]] -name = "wayland-protocols-experimental" -version = "20250721.0.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "40a1f863128dcaaec790d7b4b396cc9b9a7a079e878e18c47e6c2d2c5a8dcbb1" -dependencies = [ - "bitflags 2.10.0", - "wayland-backend", - "wayland-client", - "wayland-protocols", - "wayland-scanner", -] - -[[package]] -name = "wayland-protocols-misc" -version = "0.3.10" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "791c58fdeec5406aa37169dd815327d1e47f334219b523444bc26d70ceb4c34e" -dependencies = [ - "bitflags 2.10.0", - "wayland-backend", - "wayland-client", - "wayland-protocols", - "wayland-scanner", -] - -[[package]] -name = "wayland-protocols-plasma" -version = "0.3.10" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "aa98634619300a535a9a97f338aed9a5ff1e01a461943e8346ff4ae26007306b" -dependencies = [ - "bitflags 2.10.0", - "wayland-backend", - "wayland-client", - "wayland-protocols", - "wayland-scanner", -] - -[[package]] -name = "wayland-protocols-wlr" -version = "0.3.10" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e9597cdf02cf0c34cd5823786dce6b5ae8598f05c2daf5621b6e178d4f7345f3" -dependencies = [ - "bitflags 2.10.0", - "wayland-backend", - "wayland-client", - "wayland-protocols", - "wayland-scanner", - "wayland-server", -] - -[[package]] -name = "wayland-scanner" -version = "0.31.8" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5423e94b6a63e68e439803a3e153a9252d5ead12fd853334e2ad33997e3889e3" -dependencies = [ - "proc-macro2", - "quick-xml", - "quote", -] - -[[package]] -name = "wayland-server" -version = "0.31.11" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9297ab90f8d1f597711d36455c5b1b2290eca59b8134485e377a296b80b118c9" -dependencies = [ - "bitflags 2.10.0", - "downcast-rs", - "rustix 1.1.3", - "wayland-backend", - "wayland-scanner", -] - -[[package]] -name = "wayland-sys" -version = "0.31.8" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1e6dbfc3ac5ef974c92a2235805cc0114033018ae1290a72e474aa8b28cbbdfd" -dependencies = [ - "dlib", - "log", - "once_cell", - "pkg-config", -] - -[[package]] -name = "web-sys" -version = "0.3.83" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9b32828d774c412041098d182a8b38b16ea816958e07cf40eec2bc080ae137ac" -dependencies = [ - "js-sys", - "wasm-bindgen", -] - -[[package]] -name = "web-time" -version = "1.1.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5a6580f308b1fad9207618087a65c04e7a10bc77e02c8e84e9b00dd4b12fa0bb" -dependencies = [ - "js-sys", - "wasm-bindgen", -] - -[[package]] -name = "weezl" -version = "0.1.12" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a28ac98ddc8b9274cb41bb4d9d4d5c425b6020c50c46f25559911905610b4a88" - -[[package]] -name = "wgpu" -version = "22.1.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e1d1c4ba43f80542cf63a0a6ed3134629ae73e8ab51e4b765a67f3aa062eb433" -dependencies = [ - "arrayvec 0.7.6", - "cfg_aliases 0.1.1", - "document-features", - "js-sys", - "log", - "naga", - "parking_lot 0.12.5", - "profiling", - "raw-window-handle", - "smallvec", - "static_assertions", - "wasm-bindgen", - "wasm-bindgen-futures", - "web-sys", - "wgpu-core", - "wgpu-hal", - "wgpu-types", -] - -[[package]] -name = "wgpu-core" -version = "22.1.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0348c840d1051b8e86c3bcd31206080c5e71e5933dabd79be1ce732b0b2f089a" -dependencies = [ - "arrayvec 0.7.6", - "bit-vec", - "bitflags 2.10.0", - "cfg_aliases 0.1.1", - "document-features", - "indexmap", - "log", - "naga", - "once_cell", - "parking_lot 0.12.5", - "profiling", - "raw-window-handle", - "rustc-hash 1.1.0", - "smallvec", - "thiserror 1.0.69", - "wgpu-hal", - "wgpu-types", -] - -[[package]] -name = "wgpu-hal" -version = "22.0.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f6bbf4b4de8b2a83c0401d9e5ae0080a2792055f25859a02bf9be97952bbed4f" -dependencies = [ - "android_system_properties", - "arrayvec 0.7.6", - "ash", - "bit-set", - "bitflags 2.10.0", - "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", - "log", - "metal", - "naga", - "ndk-sys 0.5.0+25.2.9519653", - "objc", - "once_cell", - "parking_lot 0.12.5", - "profiling", - "range-alloc", - "raw-window-handle", - "renderdoc-sys", - "rustc-hash 1.1.0", - "smallvec", - "thiserror 1.0.69", - "wasm-bindgen", - "web-sys", - "wgpu-types", - "winapi", -] - -[[package]] -name = "wgpu-types" -version = "22.0.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "bc9d91f0e2c4b51434dfa6db77846f2793149d8e73f800fa2e41f52b8eac3c5d" -dependencies = [ - "bitflags 2.10.0", - "js-sys", - "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" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5c839a674fcd7a98952e593242ea400abe93992746761e38641405d28b00f419" -dependencies = [ - "winapi-i686-pc-windows-gnu", - "winapi-x86_64-pc-windows-gnu", -] - -[[package]] -name = "winapi-i686-pc-windows-gnu" -version = "0.4.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ac3b87c63620426dd9b991e5ce0329eff545bccbbb34f3be09ff6fb6ab51b7b6" - -[[package]] -name = "winapi-util" -version = "0.1.11" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c2a7b1c03c876122aa43f3020e6c3c3ee5c05081c9a00739faf7503aeba10d22" -dependencies = [ - "windows-sys 0.61.2", -] - -[[package]] -name = "winapi-x86_64-pc-windows-gnu" -version = "0.4.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "712e227841d057c1ee1cd2fb22fa7e5a5461ae8e48fa2ca79ec42cfc1931183f" - -[[package]] -name = "window_clipboard" -version = "0.4.1" -source = "git+https://github.com/pop-os/window_clipboard.git?tag=pop-0.13-2#6b9faab87bea9cebec6ae036906fd67fed254f5f" -dependencies = [ - "clipboard-win", - "clipboard_macos", - "clipboard_wayland", - "clipboard_x11", - "dnd", - "mime", - "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" -version = "0.54.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9252e5725dbed82865af151df558e754e4a3c2c30818359eb17465f1346a1b49" -dependencies = [ - "windows-core 0.54.0", - "windows-implement 0.53.0", - "windows-interface 0.53.0", - "windows-targets 0.52.6", -] - -[[package]] -name = "windows-core" -version = "0.52.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "33ab640c8d7e35bf8ba19b884ba838ceb4fba93a4e8c65a9059d08afcfc683d9" -dependencies = [ - "windows-targets 0.52.6", -] - -[[package]] -name = "windows-core" -version = "0.54.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "12661b9c89351d684a50a8a643ce5f608e20243b9fb84687800163429f161d65" -dependencies = [ - "windows-result 0.1.2", - "windows-targets 0.52.6", -] - -[[package]] -name = "windows-core" -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-link", - "windows-result 0.4.1", - "windows-strings", -] - -[[package]] -name = "windows-implement" -version = "0.53.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "942ac266be9249c84ca862f0a164a39533dc2f6f33dc98ec89c8da99b82ea0bd" -dependencies = [ - "proc-macro2", - "quote", - "syn 2.0.114", -] - -[[package]] -name = "windows-implement" -version = "0.60.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "053e2e040ab57b9dc951b72c264860db7eb3b0200ba345b4e4c3b14f67855ddf" -dependencies = [ - "proc-macro2", - "quote", - "syn 2.0.114", -] - -[[package]] -name = "windows-interface" -version = "0.53.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "da33557140a288fae4e1d5f8873aaf9eb6613a9cf82c3e070223ff177f598b60" -dependencies = [ - "proc-macro2", - "quote", - "syn 2.0.114", -] - -[[package]] -name = "windows-interface" -version = "0.59.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3f316c4a2570ba26bbec722032c4099d8c8bc095efccdc15688708623367e358" -dependencies = [ - "proc-macro2", - "quote", - "syn 2.0.114", -] - -[[package]] -name = "windows-link" -version = "0.2.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f0805222e57f7521d6a62e36fa9163bc891acd422f971defe97d64e70d0a4fe5" - -[[package]] -name = "windows-result" -version = "0.1.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5e383302e8ec8515204254685643de10811af0ed97ea37210dc26fb0032647f8" -dependencies = [ - "windows-targets 0.52.6", -] - -[[package]] -name = "windows-result" -version = "0.4.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7781fa89eaf60850ac3d2da7af8e5242a5ea78d1a11c49bf2910bb5a73853eb5" -dependencies = [ - "windows-link", -] - -[[package]] -name = "windows-strings" -version = "0.5.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7837d08f69c77cf6b07689544538e017c1bfcf57e34b4c0ff58e6c2cd3b37091" -dependencies = [ - "windows-link", -] - -[[package]] -name = "windows-sys" -version = "0.45.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "75283be5efb2831d37ea142365f009c02ec203cd29a3ebecbc093d52315b66d0" -dependencies = [ - "windows-targets 0.42.2", -] - -[[package]] -name = "windows-sys" -version = "0.48.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "677d2418bec65e3338edb076e806bc1ec15693c5d0104683f2efe857f61056a9" -dependencies = [ - "windows-targets 0.48.5", -] - -[[package]] -name = "windows-sys" -version = "0.52.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "282be5f36a8ce781fad8c8ae18fa3f9beff57ec1b52cb3de0789201425d9a33d" -dependencies = [ - "windows-targets 0.52.6", -] - -[[package]] -name = "windows-sys" -version = "0.59.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1e38bc4d79ed67fd075bcc251a1c39b32a1776bbe92e5bef1f0bf1f8c531853b" -dependencies = [ - "windows-targets 0.52.6", -] - -[[package]] -name = "windows-sys" -version = "0.60.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f2f500e4d28234f72040990ec9d39e3a6b950f9f22d3dba18416c35882612bcb" -dependencies = [ - "windows-targets 0.53.5", -] - -[[package]] -name = "windows-sys" -version = "0.61.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ae137229bcbd6cdf0f7b80a31df61766145077ddf49416a728b02cb3921ff3fc" -dependencies = [ - "windows-link", -] - -[[package]] -name = "windows-targets" -version = "0.42.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8e5180c00cd44c9b1c88adb3693291f1cd93605ded80c250a75d472756b4d071" -dependencies = [ - "windows_aarch64_gnullvm 0.42.2", - "windows_aarch64_msvc 0.42.2", - "windows_i686_gnu 0.42.2", - "windows_i686_msvc 0.42.2", - "windows_x86_64_gnu 0.42.2", - "windows_x86_64_gnullvm 0.42.2", - "windows_x86_64_msvc 0.42.2", -] - -[[package]] -name = "windows-targets" -version = "0.48.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9a2fa6e2155d7247be68c096456083145c183cbbbc2764150dda45a87197940c" -dependencies = [ - "windows_aarch64_gnullvm 0.48.5", - "windows_aarch64_msvc 0.48.5", - "windows_i686_gnu 0.48.5", - "windows_i686_msvc 0.48.5", - "windows_x86_64_gnu 0.48.5", - "windows_x86_64_gnullvm 0.48.5", - "windows_x86_64_msvc 0.48.5", -] - -[[package]] -name = "windows-targets" -version = "0.52.6" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9b724f72796e036ab90c1021d4780d4d3d648aca59e491e6b98e725b84e99973" -dependencies = [ - "windows_aarch64_gnullvm 0.52.6", - "windows_aarch64_msvc 0.52.6", - "windows_i686_gnu 0.52.6", - "windows_i686_gnullvm 0.52.6", - "windows_i686_msvc 0.52.6", - "windows_x86_64_gnu 0.52.6", - "windows_x86_64_gnullvm 0.52.6", - "windows_x86_64_msvc 0.52.6", -] - -[[package]] -name = "windows-targets" -version = "0.53.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4945f9f551b88e0d65f3db0bc25c33b8acea4d9e41163edf90dcd0b19f9069f3" -dependencies = [ - "windows-link", - "windows_aarch64_gnullvm 0.53.1", - "windows_aarch64_msvc 0.53.1", - "windows_i686_gnu 0.53.1", - "windows_i686_gnullvm 0.53.1", - "windows_i686_msvc 0.53.1", - "windows_x86_64_gnu 0.53.1", - "windows_x86_64_gnullvm 0.53.1", - "windows_x86_64_msvc 0.53.1", -] - -[[package]] -name = "windows_aarch64_gnullvm" -version = "0.42.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "597a5118570b68bc08d8d59125332c54f1ba9d9adeedeef5b99b02ba2b0698f8" - -[[package]] -name = "windows_aarch64_gnullvm" -version = "0.48.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2b38e32f0abccf9987a4e3079dfb67dcd799fb61361e53e2882c3cbaf0d905d8" - -[[package]] -name = "windows_aarch64_gnullvm" -version = "0.52.6" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "32a4622180e7a0ec044bb555404c800bc9fd9ec262ec147edd5989ccd0c02cd3" - -[[package]] -name = "windows_aarch64_gnullvm" -version = "0.53.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a9d8416fa8b42f5c947f8482c43e7d89e73a173cead56d044f6a56104a6d1b53" - -[[package]] -name = "windows_aarch64_msvc" -version = "0.42.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e08e8864a60f06ef0d0ff4ba04124db8b0fb3be5776a5cd47641e942e58c4d43" - -[[package]] -name = "windows_aarch64_msvc" -version = "0.48.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "dc35310971f3b2dbbf3f0690a219f40e2d9afcf64f9ab7cc1be722937c26b4bc" - -[[package]] -name = "windows_aarch64_msvc" -version = "0.52.6" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "09ec2a7bb152e2252b53fa7803150007879548bc709c039df7627cabbd05d469" - -[[package]] -name = "windows_aarch64_msvc" -version = "0.53.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b9d782e804c2f632e395708e99a94275910eb9100b2114651e04744e9b125006" - -[[package]] -name = "windows_i686_gnu" -version = "0.42.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c61d927d8da41da96a81f029489353e68739737d3beca43145c8afec9a31a84f" - -[[package]] -name = "windows_i686_gnu" -version = "0.48.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a75915e7def60c94dcef72200b9a8e58e5091744960da64ec734a6c6e9b3743e" - -[[package]] -name = "windows_i686_gnu" -version = "0.52.6" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8e9b5ad5ab802e97eb8e295ac6720e509ee4c243f69d781394014ebfe8bbfa0b" - -[[package]] -name = "windows_i686_gnu" -version = "0.53.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "960e6da069d81e09becb0ca57a65220ddff016ff2d6af6a223cf372a506593a3" - -[[package]] -name = "windows_i686_gnullvm" -version = "0.52.6" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0eee52d38c090b3caa76c563b86c3a4bd71ef1a819287c19d586d7334ae8ed66" - -[[package]] -name = "windows_i686_gnullvm" -version = "0.53.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "fa7359d10048f68ab8b09fa71c3daccfb0e9b559aed648a8f95469c27057180c" - -[[package]] -name = "windows_i686_msvc" -version = "0.42.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "44d840b6ec649f480a41c8d80f9c65108b92d89345dd94027bfe06ac444d1060" - -[[package]] -name = "windows_i686_msvc" -version = "0.48.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8f55c233f70c4b27f66c523580f78f1004e8b5a8b659e05a4eb49d4166cca406" - -[[package]] -name = "windows_i686_msvc" -version = "0.52.6" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "240948bc05c5e7c6dabba28bf89d89ffce3e303022809e73deaefe4f6ec56c66" - -[[package]] -name = "windows_i686_msvc" -version = "0.53.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1e7ac75179f18232fe9c285163565a57ef8d3c89254a30685b57d83a38d326c2" - -[[package]] -name = "windows_x86_64_gnu" -version = "0.42.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8de912b8b8feb55c064867cf047dda097f92d51efad5b491dfb98f6bbb70cb36" - -[[package]] -name = "windows_x86_64_gnu" -version = "0.48.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "53d40abd2583d23e4718fddf1ebec84dbff8381c07cae67ff7768bbf19c6718e" - -[[package]] -name = "windows_x86_64_gnu" -version = "0.52.6" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "147a5c80aabfbf0c7d901cb5895d1de30ef2907eb21fbbab29ca94c5b08b1a78" - -[[package]] -name = "windows_x86_64_gnu" -version = "0.53.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9c3842cdd74a865a8066ab39c8a7a473c0778a3f29370b5fd6b4b9aa7df4a499" - -[[package]] -name = "windows_x86_64_gnullvm" -version = "0.42.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "26d41b46a36d453748aedef1486d5c7a85db22e56aff34643984ea85514e94a3" - -[[package]] -name = "windows_x86_64_gnullvm" -version = "0.48.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0b7b52767868a23d5bab768e390dc5f5c55825b6d30b86c844ff2dc7414044cc" - -[[package]] -name = "windows_x86_64_gnullvm" -version = "0.52.6" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "24d5b23dc417412679681396f2b49f3de8c1473deb516bd34410872eff51ed0d" - -[[package]] -name = "windows_x86_64_gnullvm" -version = "0.53.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0ffa179e2d07eee8ad8f57493436566c7cc30ac536a3379fdf008f47f6bb7ae1" - -[[package]] -name = "windows_x86_64_msvc" -version = "0.42.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9aec5da331524158c6d1a4ac0ab1541149c0b9505fde06423b02f5ef0106b9f0" - -[[package]] -name = "windows_x86_64_msvc" -version = "0.48.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ed94fce61571a4006852b7389a063ab983c02eb1bb37b47f8272ce92d06d9538" - -[[package]] -name = "windows_x86_64_msvc" -version = "0.52.6" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "589f6da84c646204747d1270a2a5661ea66ed1cced2631d546fdfb155959f9ec" - -[[package]] -name = "windows_x86_64_msvc" -version = "0.53.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d6bbff5f0aada427a1e5a6da5f1f98158182f26556f345ac9e04d36d0ebed650" - -[[package]] -name = "winit" -version = "0.30.5" -source = "git+https://github.com/pop-os/winit.git?tag=iced-xdg-surface-0.13-rc#12a5f17d1811cdebbcbd310a3d92965e9142fa12" -dependencies = [ - "ahash", - "android-activity", - "atomic-waker", - "bitflags 2.10.0", - "block2 0.5.1", - "bytemuck", - "calloop 0.13.0", - "cfg_aliases 0.2.1", - "concurrent-queue", - "core-foundation", - "core-graphics", - "cursor-icon", - "dpi", - "js-sys", - "libc", - "memmap2 0.9.9", - "ndk", - "objc2 0.5.2", - "objc2-app-kit 0.2.2", - "objc2-foundation 0.2.2", - "objc2-ui-kit", - "orbclient", - "percent-encoding", - "pin-project", - "raw-window-handle", - "redox_syscall 0.5.18", - "rustix 0.38.44", - "sctk-adwaita", - "smithay-client-toolkit 0.19.2", - "smol_str", - "tracing", - "unicode-segmentation", - "wasm-bindgen", - "wasm-bindgen-futures", - "wayland-backend", - "wayland-client", - "wayland-protocols", - "wayland-protocols-plasma", - "web-sys", - "web-time", - "windows-sys 0.52.0", - "x11-dl", - "x11rb", - "xkbcommon-dl", -] - -[[package]] -name = "winnow" -version = "0.5.40" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f593a95398737aeed53e489c785df13f3618e41dbcd6718c6addbf1395aa6876" -dependencies = [ - "memchr", -] - -[[package]] -name = "winnow" -version = "0.7.14" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5a5364e9d77fcdeeaa6062ced926ee3381faa2ee02d3eb83a5c27a8825540829" -dependencies = [ - "memchr", -] - -[[package]] -name = "winreg" -version = "0.9.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "16cdb3898397cf7f624c294948669beafaeebc5577d5ec53d0afb76633593597" -dependencies = [ - "winapi", -] - -[[package]] -name = "wit-bindgen" -version = "0.46.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f17a85883d4e6d00e8a97c586de764dabcc06133f7f1d55dce5cdc070ad7fe59" - -[[package]] -name = "writeable" -version = "0.6.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9edde0db4769d2dc68579893f2306b26c6ecfbe0ef499b013d731b7b9247e0b9" - -[[package]] -name = "x11-dl" -version = "2.21.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "38735924fedd5314a6e548792904ed8c6de6636285cb9fec04d5b1db85c1516f" -dependencies = [ - "libc", - "once_cell", - "pkg-config", -] - -[[package]] -name = "x11rb" -version = "0.13.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9993aa5be5a26815fe2c3eacfc1fde061fc1a1f094bf1ad2a18bf9c495dd7414" -dependencies = [ - "as-raw-xcb-connection", - "gethostname", - "libc", - "libloading", - "once_cell", - "rustix 1.1.3", - "x11rb-protocol", - "xcursor", -] - -[[package]] -name = "x11rb-protocol" -version = "0.13.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ea6fc2961e4ef194dcbfe56bb845534d0dc8098940c7e5c012a258bfec6701bd" - -[[package]] -name = "xcursor" -version = "0.3.10" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "bec9e4a500ca8864c5b47b8b482a73d62e4237670e5b5f1d6b9e3cae50f28f2b" - -[[package]] -name = "xdg" -version = "3.0.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2fb433233f2df9344722454bc7e96465c9d03bff9d77c248f9e7523fe79585b5" - -[[package]] -name = "xdg-home" -version = "1.3.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ec1cdab258fb55c0da61328dc52c8764709b249011b2cad0454c72f0bf10a1f6" -dependencies = [ - "libc", - "windows-sys 0.59.0", -] - -[[package]] -name = "xkbcommon" -version = "0.7.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "13867d259930edc7091a6c41b4ce6eee464328c6ff9659b7e4c668ca20d4c91e" -dependencies = [ - "libc", - "memmap2 0.8.0", - "xkeysym", -] - -[[package]] -name = "xkbcommon" -version = "0.8.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8d66ca9352cbd4eecbbc40871d8a11b4ac8107cfc528a6e14d7c19c69d0e1ac9" -dependencies = [ - "libc", - "memmap2 0.9.9", - "xkeysym", -] - -[[package]] -name = "xkbcommon-dl" -version = "0.4.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d039de8032a9a8856a6be89cea3e5d12fdd82306ab7c94d74e6deab2460651c5" -dependencies = [ - "bitflags 2.10.0", - "dlib", - "log", - "once_cell", - "xkeysym", -] - -[[package]] -name = "xkeysym" -version = "0.2.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b9cc00251562a284751c9973bace760d86c0276c471b4be569fe6b068ee97a56" -dependencies = [ - "bytemuck", -] - -[[package]] -name = "xml-rs" -version = "0.8.28" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3ae8337f8a065cfc972643663ea4279e04e7256de865aa66fe25cec5fb912d3f" - -[[package]] -name = "xmlwriter" -version = "0.1.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ec7a2a501ed189703dba8b08142f057e887dfc4b2cc4db2d343ac6376ba3e0b9" - -[[package]] -name = "y4m" -version = "0.8.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7a5a4b21e1a62b67a2970e6831bc091d7b87e119e7f9791aef9702e3bef04448" - -[[package]] -name = "yansi" -version = "1.0.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "cfe53a6657fd280eaa890a3bc59152892ffa3e30101319d168b781ed6529b049" - -[[package]] -name = "yazi" -version = "0.2.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e01738255b5a16e78bbb83e7fbba0a1e7dd506905cfc53f4622d89015a03fbb5" - -[[package]] -name = "yoke" -version = "0.8.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "72d6e5c6afb84d73944e5cedb052c4680d5657337201555f9f2a16b7406d4954" -dependencies = [ - "stable_deref_trait", - "yoke-derive", - "zerofrom", -] - -[[package]] -name = "yoke-derive" -version = "0.8.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b659052874eb698efe5b9e8cf382204678a0086ebf46982b79d6ca3182927e5d" -dependencies = [ - "proc-macro2", - "quote", - "syn 2.0.114", - "synstructure", -] - -[[package]] -name = "zbus" -version = "3.15.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "675d170b632a6ad49804c8cf2105d7c31eddd3312555cffd4b740e08e97c25e6" -dependencies = [ - "async-broadcast 0.5.1", - "async-process 1.8.1", - "async-recursion", - "async-trait", - "byteorder", - "derivative", - "enumflags2", - "event-listener 2.5.3", - "futures-core", - "futures-sink", - "futures-util", - "hex", - "nix", - "once_cell", - "ordered-stream", - "rand 0.8.5", - "serde", - "serde_repr", - "sha1", - "static_assertions", - "tokio", - "tracing", - "uds_windows", - "winapi", - "xdg-home", - "zbus_macros 3.15.2", - "zbus_names 2.6.1", - "zvariant 3.15.2", -] - -[[package]] -name = "zbus" -version = "5.13.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7515214ab069b46f614dee52c1256015cdc1a0b441ed612118e2871014956741" -dependencies = [ - "async-broadcast 0.7.2", - "async-executor", - "async-io 2.6.0", - "async-lock 3.4.2", - "async-process 2.5.0", - "async-recursion", - "async-task", - "async-trait", - "blocking", - "enumflags2", - "event-listener 5.4.1", - "futures-core", - "futures-lite 2.6.1", - "hex", - "libc", - "ordered-stream", - "rustix 1.1.3", - "serde", - "serde_repr", - "tokio", - "tracing", - "uds_windows", - "uuid", - "windows-sys 0.61.2", - "winnow 0.7.14", - "zbus_macros 5.13.0", - "zbus_names 4.3.1", - "zvariant 5.9.0", -] - -[[package]] -name = "zbus_macros" -version = "3.15.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7131497b0f887e8061b430c530240063d33bf9455fa34438f388a245da69e0a5" -dependencies = [ - "proc-macro-crate 1.3.1", - "proc-macro2", - "quote", - "regex", - "syn 1.0.109", - "zvariant_utils 1.0.1", -] - -[[package]] -name = "zbus_macros" -version = "5.13.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "04f54d8a5b4e9c46cf4a9732da4899b12851b5df952fc8deda23aca1d6f3e26c" -dependencies = [ - "proc-macro-crate 3.4.0", - "proc-macro2", - "quote", - "syn 2.0.114", - "zbus_names 4.3.1", - "zvariant 5.9.0", - "zvariant_utils 3.3.0", -] - -[[package]] -name = "zbus_names" -version = "2.6.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "437d738d3750bed6ca9b8d423ccc7a8eb284f6b1d6d4e225a0e4e6258d864c8d" -dependencies = [ - "serde", - "static_assertions", - "zvariant 3.15.2", -] - -[[package]] -name = "zbus_names" -version = "4.3.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ffd8af6d5b78619bab301ff3c560a5bd22426150253db278f164d6cf3b72c50f" -dependencies = [ - "serde", - "winnow 0.7.14", - "zvariant 5.9.0", -] - -[[package]] -name = "zeno" -version = "0.3.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6df3dc4292935e51816d896edcd52aa30bc297907c26167fec31e2b0c6a32524" - -[[package]] -name = "zerocopy" -version = "0.8.33" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "668f5168d10b9ee831de31933dc111a459c97ec93225beb307aed970d1372dfd" -dependencies = [ - "zerocopy-derive", -] - -[[package]] -name = "zerocopy-derive" -version = "0.8.33" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2c7962b26b0a8685668b671ee4b54d007a67d4eaf05fda79ac0ecf41e32270f1" -dependencies = [ - "proc-macro2", - "quote", - "syn 2.0.114", -] - -[[package]] -name = "zerofrom" -version = "0.1.6" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "50cc42e0333e05660c3587f3bf9d0478688e15d870fab3346451ce7f8c9fbea5" -dependencies = [ - "zerofrom-derive", -] - -[[package]] -name = "zerofrom-derive" -version = "0.1.6" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d71e5d6e06ab090c67b5e44993ec16b72dcbaabc526db883a360057678b48502" -dependencies = [ - "proc-macro2", - "quote", - "syn 2.0.114", - "synstructure", -] - -[[package]] -name = "zerotrie" -version = "0.2.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2a59c17a5562d507e4b54960e8569ebee33bee890c70aa3fe7b97e85a9fd7851" -dependencies = [ - "displaydoc", - "yoke", - "zerofrom", -] - -[[package]] -name = "zerovec" -version = "0.11.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6c28719294829477f525be0186d13efa9a3c602f7ec202ca9e353d310fb9a002" -dependencies = [ - "serde", - "yoke", - "zerofrom", - "zerovec-derive", -] - -[[package]] -name = "zerovec-derive" -version = "0.11.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "eadce39539ca5cb3985590102671f2567e659fca9666581ad3411d59207951f3" -dependencies = [ - "proc-macro2", - "quote", - "syn 2.0.114", -] - -[[package]] -name = "zmij" -version = "1.0.12" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2fc5a66a20078bf1251bde995aa2fdcc4b800c70b5d92dd2c62abc5c60f679f8" - -[[package]] -name = "zune-core" -version = "0.4.12" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3f423a2c17029964870cfaabb1f13dfab7d092a62a29a89264f4d36990ca414a" - -[[package]] -name = "zune-core" -version = "0.5.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "111f7d9820f05fd715df3144e254d6fc02ee4088b0644c0ffd0efc9e6d9d2773" - -[[package]] -name = "zune-inflate" -version = "0.2.54" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "73ab332fe2f6680068f3582b16a24f90ad7096d5d39b974d1c0aff0125116f02" -dependencies = [ - "simd-adler32", -] - -[[package]] -name = "zune-jpeg" -version = "0.4.21" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "29ce2c8a9384ad323cf564b67da86e21d3cfdff87908bc1223ed5c99bc792713" -dependencies = [ - "zune-core 0.4.12", -] - -[[package]] -name = "zune-jpeg" -version = "0.5.8" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e35aee689668bf9bd6f6f3a6c60bb29ba1244b3b43adfd50edd554a371da37d5" -dependencies = [ - "zune-core 0.5.0", -] - -[[package]] -name = "zvariant" -version = "3.15.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4eef2be88ba09b358d3b58aca6e41cd853631d44787f319a1383ca83424fb2db" -dependencies = [ - "byteorder", - "enumflags2", - "libc", - "serde", - "static_assertions", - "zvariant_derive 3.15.2", -] - -[[package]] -name = "zvariant" -version = "5.9.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "788ca131e3757991e4b9fe9f7b78ae302749ed96093ff60858a1f4732b04b164" -dependencies = [ - "endi", - "enumflags2", - "serde", - "url", - "winnow 0.7.14", - "zvariant_derive 5.9.0", - "zvariant_utils 3.3.0", -] - -[[package]] -name = "zvariant_derive" -version = "3.15.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "37c24dc0bed72f5f90d1f8bb5b07228cbf63b3c6e9f82d82559d4bae666e7ed9" -dependencies = [ - "proc-macro-crate 1.3.1", - "proc-macro2", - "quote", - "syn 1.0.109", - "zvariant_utils 1.0.1", -] - -[[package]] -name = "zvariant_derive" -version = "5.9.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0e69a2f6b221a6fec9bd6bcc77c19360cca106f92a5fd948b8aa17d2339c7505" -dependencies = [ - "proc-macro-crate 3.4.0", - "proc-macro2", - "quote", - "syn 2.0.114", - "zvariant_utils 3.3.0", -] - -[[package]] -name = "zvariant_utils" -version = "1.0.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7234f0d811589db492d16893e3f21e8e2fd282e6d01b0cddee310322062cc200" -dependencies = [ - "proc-macro2", - "quote", - "syn 1.0.109", -] - -[[package]] -name = "zvariant_utils" -version = "3.3.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f75c23a64ef8f40f13a6989991e643554d9bef1d682a281160cf0c1bc389c5e9" -dependencies = [ - "proc-macro2", - "quote", - "serde", - "syn 2.0.114", - "winnow 0.7.14", -] - -[[patch.unused]] -name = "cosmic-config" -version = "1.0.0" - -[[patch.unused]] -name = "cosmic-theme" -version = "1.0.0" - -[[patch.unused]] -name = "libcosmic-yoda" -version = "0.1.0-yoda.2" diff --git a/Cargo.toml b/Cargo.toml index c81d358..986a96f 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,44 +1,38 @@ # SPDX-License-Identifier: GPL-3.0-or-later # Cargo.toml # -# Project metadata and dependencies for the Noctua document viewer. +# Noctua COSMIC desktop application [package] name = "noctua" version = "0.1.0" -edition = "2024" -description = "A wise document and image viewer for the COSMIC™ desktop" +edition = "2021" +description = "Noctua – a document viewer for the COSMIC™ desktop" repository = "https://codeberg.org/wfx/noctua" authors = ["Wolfgang Morawetz "] license = "GPL-3.0-or-later" -keywords = ["document", "image", "viewer", "pdf", "cosmic"] -categories = ["gui", "multimedia::graphics", "multimedia::images"] [features] default = ["image", "vector", "portable"] image = ["dep:image", "dep:kamadak-exif"] vector = ["dep:resvg"] -portable = ["dep:poppler", "dep:cairo-rs"] +portable = ["dep:pdfium-render"] full = ["image", "vector", "portable"] [dependencies] -# Error handling -anyhow = "1" +# Serialization +serde = { version = "1", features = ["derive"] } +ron = "0.8" -# Feature-gated dependencies -kamadak-exif = { version = "0.5.5", optional = true } -image = { version = "0.25.9", optional = true } -poppler = { version = "0.4", features = ["render"], optional = true } -cairo-rs = { version = "0.18", features = ["png"], optional = true } +# Feature-gated document backends +image = { version = "0.25", optional = true } +kamadak-exif = { version = "0.5", optional = true } +pdfium-render = { version = "0.8", optional = true } resvg = { version = "0.45", optional = true } -# Async / concurrency -futures-util = "0.3.31" -tokio = { version = "1.48.0", features = ["full"] } - # Logging -log = "0.4.20" -simple_logger = "4.2.0" +log = "0.4" +env_logger = "0.11" # i18n / localization i18n-embed = { version = "0.16", features = [ @@ -47,41 +41,35 @@ i18n-embed = { version = "0.16", features = [ ] } i18n-embed-fl = "0.10" -# Misc utilities -open = "5.3.2" -rust-embed = "8.8.0" -dirs = "5.0" -sha2 = "0.10" -clap = { version = "4.5.54", features = ["derive"] } -env_logger = "0.11.8" -wallpaper = "3.2" +# Resource embedding +rust-embed = "8" -[dependencies.libcosmic] -git = "https://github.com/pop-os/libcosmic.git" -# See https://github.com/pop-os/libcosmic/blob/master/Cargo.toml for available features. +# Open files/URLs with the system default application +open = "5" + +# Thumbnail cache (freedesktop.org spec) +dirs = "5.0" +md5 = "0.7" +png = "0.17" + +# CLI argument parsing +clap = { version = "4", features = ["derive"] } + +# Async runtime (required by libcosmic) +tokio = { version = "1", features = ["rt", "rt-multi-thread", "macros"] } + +[dependencies.cosmic] +package = "libcosmic-yoda" +path = "../libcosmic" +default-features = false features = [ - # Accessibility support "a11y", - # About widget for the app "about", - # Uses cosmic-settings-daemon to watch for config file changes "dbus-config", - # Support creating additional application windows. "multi-window", - # On app startup, focuses an existing instance if the app is already open "single-instance", - # Uses tokio as the executor for the runtime "tokio", - # Windowing support for X11, Windows, Mac, & Redox "winit", - # Add Wayland support to winit "wayland", - # GPU-accelerated rendering "wgpu", ] - -# Uncomment to test a locally-cloned libcosmic -[patch.'https://github.com/pop-os/libcosmic'] -libcosmic-yoda = { path = "../libcosmic" } -cosmic-config = { path = "../libcosmic/cosmic-config" } -cosmic-theme = { path = "../libcosmic/cosmic-theme" } diff --git a/README.md b/README.md deleted file mode 100644 index b5f4c3e..0000000 --- a/README.md +++ /dev/null @@ -1,2 +0,0 @@ -# noctua - diff --git a/docs/features.md b/docs/features.md deleted file mode 100644 index 5cd123e..0000000 --- a/docs/features.md +++ /dev/null @@ -1,202 +0,0 @@ -# Noctua Features - -This document describes the implemented and planned features of Noctua, a modern image viewer for the COSMIC desktop environment. - -## Current Features - -### Document Support - -#### Raster Images (Implemented) -- **Formats**: PNG, JPEG, GIF, BMP, TIFF, WebP, and all formats supported by `image-rs` -- **Capabilities**: - - Full pixel-perfect rendering at 100% zoom - - Lossless transformations (rotate, flip) - - Real-time transformation preview - - EXIF metadata extraction - -#### Vector Graphics (Implemented) -- **Formats**: SVG -- **Rendering**: High-quality rendering via `resvg` library -- **Capabilities**: - - Scalable display at any zoom level without quality loss - - Transformations (rotate, flip) - -#### Portable Documents (Implemented) -- **Formats**: PDF -- **Rendering**: Full PDF rendering via poppler library -- **Multi-page navigation**: Browse through all pages of a document -- **Page thumbnails**: Left sidebar shows page previews (generated on demand) -- **Transformations**: Rotate and flip on rendered pages - -### Navigation - -#### Folder Navigation (Implemented) -- **Automatic folder scanning**: When opening an image, all supported images in the same folder are indexed -- **Quick navigation**: - - Arrow keys (Left/Right) to navigate between images - - Footer displays current position (e.g., "3 / 42") - - Seamless transitions between images - -#### File Opening (Implemented) -- **Command-line arguments**: Open images directly from terminal -- **Default directory**: Configurable starting location (defaults to XDG Pictures) -- File dialog not yet implemented - -### View Controls - -#### Zoom (Implemented) -- **Mouse wheel**: Zoom in/out centered on cursor position -- **Keyboard shortcuts**: - - `+` or `=` - Zoom in - - `-` - Zoom out - - `1` - Reset to 100% (Actual Size) - - `f` - Fit to window -- **View modes**: - - **Fit**: Automatically scales image to fit window while preserving aspect ratio - - **Actual Size**: Displays image at 100% (1:1 pixel mapping) - - **Custom**: Any zoom level from 10% to 2000% -- **Footer display**: Real-time zoom percentage or "Fit" indicator - -#### Pan (Implemented) -- **Mouse drag**: Click and drag to pan around zoomed images -- **Keyboard shortcuts**: `Ctrl + Arrow Keys` for precise panning -- **Smart boundaries**: Pan is automatically limited to image boundaries -- **Auto-center**: Images smaller than viewport are automatically centered - -#### Bidirectional State Sync (Implemented) -- Mouse interactions update keyboard/button controls -- Keyboard/button controls update mouse interaction state -- No conflicts between input methods - -### Transformations - -#### Image Manipulation (Implemented) -- **Rotate**: - - `r` - Rotate 90° clockwise - - `Shift + r` - Rotate 90° counter-clockwise - - Toolbar buttons available -- **Flip**: - - `h` - Flip horizontally (mirror) - - `v` - Flip vertically - - Toolbar buttons available -- **Lossless operations**: All transformations preserve original image quality -- **Real-time preview**: Changes are immediately visible - -### User Interface - -#### COSMIC Integration (Implemented) -- **Native COSMIC design**: Follows COSMIC desktop design language -- **Theme support**: Automatically adapts to system light/dark theme -- **Header toolbar**: - - Left: Navigation controls (Previous/Next) and panel toggle - - Center: Transformation buttons (Rotate, Flip) - horizontally centered - - Right: Information panel toggle -- **Footer bar**: - - Zoom controls with buttons - - Current zoom level display - - Image dimensions - - Navigation position counter - -#### Panels (Implemented) -- **Properties panel**: - - Image metadata display - - File information - - Action buttons: - - Set as Wallpaper (works with COSMIC, GNOME, KDE, XFCE, and tiling WMs) - - Open With… (planned) - - Show in Folder (planned) - - Toggle with `i` key or toolbar button -- **Navigation panel** (Left sidebar): - - Toggle with `n` key or toolbar button - - For multi-page documents (PDF): Shows page thumbnails - - Click to navigate to specific page - -#### Keyboard Shortcuts (Implemented) -Full keyboard-driven workflow: -- Navigation: `←` `→` -- Zoom: `+` `-` `1` `f` -- Pan: `Ctrl + ←` `Ctrl + →` `Ctrl + ↑` `Ctrl + ↓` -- Transform: `r` `Shift+r` `h` `v` -- Panels: `i` `n` -- Actions: `w` (Set as Wallpaper) - -### Desktop Integration - -#### Wallpaper Support (Implemented) -- **Set as Wallpaper**: One-click wallpaper setting with cross-desktop compatibility -- **Supported desktop environments**: - - COSMIC Desktop (direct config file integration) - - GNOME (via gsettings) - - KDE Plasma (via wallpaper crate) - - XFCE (via wallpaper crate) - - Tiling window managers (via feh) -- **Multiple access methods**: - - Keyboard shortcut: `w` - - Icon button in Properties panel - - Tooltip support for discoverability -- **Automatic fallback**: Tries multiple methods until one succeeds - -### Configuration - -#### Persistent Settings (Implemented) -- **Panel states**: Remembers which panels were open -- **Default directory**: Customizable starting location -- **Settings location**: `~/.config/noctua/config.toml` - -### Technical Features - -#### Architecture (Implemented) -- **Clean separation**: View layer agnostic to document format -- **Polymorphic documents**: Single `DocumentContent` interface for all formats -- **Efficient rendering**: Leverages COSMIC's iced renderer -- **Type-safe transformations**: Compile-time guarantees for image operations - -#### Performance (Implemented) -- **Lazy loading**: Images loaded on-demand -- **Efficient folder scanning**: Fast directory traversal -- **Minimal memory footprint**: Only active document kept in memory -- **Smooth zooming**: Hardware-accelerated rendering - -## Planned Features - -### High Priority - -#### File Operations -- File dialog integration (OpenPath message prepared) -- Save transformed images -- Copy/Move/Delete operations -- Drag-and-drop support - -### Medium Priority - -#### Multi-format TIFF Support -- Multi-page TIFF navigation (infrastructure ready) -- Page thumbnails for TIFF (same as PDF) - -#### Metadata Editing -- EXIF data modification -- Comment annotations -- Tag management - -### Low Priority - -#### Advanced Editing -- Crop tool (message prepared) -- Scale/Resize tool (message prepared) -- Basic color adjustments (brightness, contrast) - -#### Enhanced Navigation -- Grid view for folder contents -- Quick jump to file - -#### Slideshow Mode -- Auto-advance timer -- Configurable intervals -- Fullscreen support - -## Feature Status Legend - -- **Implemented**: Fully functional and tested -- **Planned**: Design complete, implementation pending -- **Partial**: Basic functionality exists, enhancements needed -- **In Progress**: Currently being developed diff --git a/docs/images/screenshot.png b/docs/images/screenshot.png deleted file mode 100644 index 3a374f4..0000000 Binary files a/docs/images/screenshot.png and /dev/null differ diff --git a/docs/usage.md b/docs/usage.md deleted file mode 100644 index 72d032b..0000000 --- a/docs/usage.md +++ /dev/null @@ -1,193 +0,0 @@ -# Usage Guide - -Noctua is a modern image viewer for the COSMIC desktop environment with full keyboard support. - -## Opening Images - -### Command Line -Open an image directly from the terminal: -```bash -noctua /path/to/image.png -``` - -When you open an image, Noctua automatically scans the folder and indexes all supported images for quick navigation. - -### Supported Formats -- **Raster Images**: PNG, JPEG, GIF, BMP, TIFF, WebP, and all formats supported by `image-rs` -- **Vector Graphics**: SVG (with scalable rendering) -- **Portable Documents**: PDF (with multi-page navigation and thumbnails) - -## Keyboard Shortcuts - -All keyboard shortcuts are case-insensitive unless otherwise noted. - -### Navigation - -| Key | Action | Description | -|:------|:------------------|:-----------------------------------------------| -| `←` | Previous image | Navigate to the previous image in the folder | -| `→` | Next image | Navigate to the next image in the folder | - -The footer shows your current position (e.g., "3 / 42"). - -For multi-page documents (PDF): -- Click thumbnails in the left sidebar to jump to a specific page -- Use `←` `→` to navigate between pages - -### Zoom and View - -| Key | Action | Description | -|:----------|:---------------------------|:------------------------------------------------------| -| `+` / `=` | Zoom in | Increase zoom by 10% | -| `-` | Zoom out | Decrease zoom by ~9% | -| `1` | Actual size (100%) | Display image at pixel-perfect 1:1 scale | -| `f` | Fit to window | Scale image to fit the window while preserving ratio | - -You can also zoom with the **mouse wheel** - the zoom centers on your cursor position. - -The current zoom level is displayed in the footer (e.g., "150%" or "Fit"). - -### Pan - -Pan controls allow you to move around zoomed images: - -| Key | Action | Description | -|:-----------|:-------------------|:-----------------------------------------| -| `Ctrl + ←` | Pan left | Move view to the left | -| `Ctrl + →` | Pan right | Move view to the right | -| `Ctrl + ↑` | Pan up | Move view upward | -| `Ctrl + ↓` | Pan down | Move view downward | -| `0` | Reset pan | Center the image | - -You can also **click and drag** with the mouse to pan around zoomed images. - -### Transformations - -| Key | Action | Description | -|:------------|:-------------------------------|:------------------------------------------| -| `h` | Flip horizontal | Mirror the image horizontally | -| `v` | Flip vertical | Flip the image upside down | -| `r` | Rotate clockwise | Rotate 90° clockwise | -| `Shift + r` | Rotate counter-clockwise | Rotate 90° counter-clockwise | - -All transformations are lossless and show in real-time. - -### Panels and UI - -| Key | Action | Description | -|:----|:-----------------------|:-----------------------------------------| -| `i` | Toggle properties | Show/hide the properties panel (metadata)| -| `n` | Toggle navigation | Show/hide the navigation sidebar | - -For multi-page documents (PDF), the navigation sidebar displays page thumbnails. -Click on a thumbnail to jump to that page. - -### Actions - -| Key | Action | Description | -|:----|:-----------------------|:-----------------------------------------| -| `w` | Set as wallpaper | Set the current image as desktop wallpaper| - -## Mouse Controls - -### Zoom -- **Mouse wheel up/down**: Zoom in/out centered on cursor -- **Footer buttons**: Click zoom in/out buttons for step-by-step control - -### Pan -- **Click and drag**: Pan around zoomed images -- Hold and drag anywhere on the image to move the view - -### Navigation -- **Footer navigation**: Use Previous/Next buttons to browse images - -## Toolbar - -The header toolbar provides quick access to common operations: - -### Left Side -- **Navigation toggle**: Show/hide the sidebar -- **Previous/Next buttons**: Navigate between images in the folder - -### Center (Horizontally Centered) -- **Rotate buttons**: Rotate clockwise or counter-clockwise -- **Flip buttons**: Flip horizontally or vertically - -### Right Side -- **Properties toggle**: Show/hide the metadata panel - -## Properties Panel - -The properties panel (toggle with `i`) displays image metadata and provides quick actions: - -### Action Buttons -Located at the top-right of the properties panel: - -- **Set as Wallpaper** (`w` key): Set the current image as your desktop wallpaper - - Works with COSMIC, GNOME, KDE, XFCE, and tiling window managers - - Automatically detects your desktop environment - - Falls back to alternative methods if the primary method fails - -- **Open With** (planned): Open the image with another application - -- **Show in Folder** (planned): Open the containing folder in your file manager - -### Metadata Display -- **File Information**: Name, format, dimensions, file size, color type -- **Camera Information** (if available): Camera model, date taken, exposure settings, GPS location - -## Footer Information - -The footer displays useful information: -- **Zoom controls**: Zoom out, current zoom level, zoom in, fit buttons -- **Image dimensions**: Width × Height in pixels -- **Navigation position**: Current image / Total images in folder - -For PDFs, it also shows the current page number. - -## Tips and Tricks - -### Keyboard-Driven Workflow -Noctua is designed for efficient keyboard use: -1. Open an image from terminal -2. Use `←` `→` to browse through the folder -3. Press `r` to rotate, `h` or `v` to flip -4. Use `+` `-` to zoom, `Ctrl + arrows` to pan -5. Press `i` to check metadata - -### Zoom and Pan Together -- Zoom with mouse wheel while hovering over the area you want to examine -- The zoom centers on your cursor, making it easy to focus on details -- Once zoomed, drag with mouse or use `Ctrl + arrows` to navigate - -### Bidirectional Controls -Mouse and keyboard work together seamlessly: -- Zoom with keyboard, pan with mouse -- Zoom with mouse wheel, the footer updates automatically -- Pan with keyboard, continue with mouse drag - -## Configuration - -Settings are stored in `~/.config/cosmic/org.codeberg.wfx.Noctua/v1/`. - -### Configurable Options -- **Default directory**: Set your preferred starting location -- **Panel states**: Your panel preferences are remembered between sessions - -## Planned Features - -The following features are prepared in code but not yet implemented: - -### File Operations -- File open dialog -- Save transformed images -- (Copy/Move/)Delete operations - -### Document Support -- SVG rendering (implemented) - -### Advanced Editing -- Crop mode (`c` key prepared) -- Scale/Resize mode (`s` key prepared) - -See [features.md](features.md) for a complete list of planned features. diff --git a/i18n/cs/noctua.ftl b/i18n/cs/noctua.ftl deleted file mode 100644 index 7ab4e97..0000000 --- a/i18n/cs/noctua.ftl +++ /dev/null @@ -1,121 +0,0 @@ -# SPDX-License-Identifier: GPL-3.0-or-later -# i18n/en/noctua.ftl -# -# Localization strings for Noctua (English). -# Usage: fl!("message-id", arg1, arg2, ...) -# -# Positional arguments ($1, $2, ...) are used for variable content. - - -## Application -noctua-app-name = Noctua -noctua-app-description = Prohlížeč dokumentů a obrázků pro prostředí COSMIC - - -## Main window -window-title = { $filename -> - [none] Noctua - *[some] { $filename } — Noctua -} - - -## Menu entries -menu-file-open = Otevřít… -menu-file-quit = Ukončit -menu-view-zoom-in = Přiblížit -menu-view-zoom-out = Oddálit -menu-view-zoom-reset = Obnovit přiblížení -menu-view-zoom-fit = Přizpůsobit oknu -menu-view-flip-horizontal = Překlopit vodorovně -menu-view-flip-vertical = Překlopit svisle -menu-view-rotate-cw = Otočit po směru hodinových ručiček -menu-view-rotate-ccw = Otočit proti směru hodinových ručiček - - -## Tooltips (for buttons and icons) -tooltip-nav-previous = Předchozí dokument -tooltip-nav-next = Další dokument -tooltip-nav-toggle = Přepnout navigační panel -tooltip-zoom-in = Přiblížit -tooltip-zoom-out = Oddálit -tooltip-zoom-fit = Přizpůsobit oknu -tooltip-rotate-ccw = Otočit proti směru hodinových ručiček -tooltip-rotate-cw = Otočit po směru hodinových ručiček -tooltip-flip-horizontal = Překlopit vodorovně -tooltip-flip-vertical = Překlopit svisle -tooltip-info-panel = Přepnout informační panel - - -## Footer / Status bar -status-zoom-fit = Přizpůsobit -status-zoom-percent = { $percent }% -status-doc-dimensions = { $width } × { $height } -status-nav-position = { $current } / { $total } -status-separator = | - - -## Placeholders / Empty states -no-document = Není načten žádný dokument - - -## Labels -label-zoom = Přiblížení -label-tools = Nástroje -label-crop = Oříznout -label-scale = Měřítko -label-page = Stránka -label-pages = Stránky - - -## Loading states -loading-metadata = Načítání metadat… -loading-thumbnails = Načítání { $current } / { $total }… - - -## Error messages -error-failed-to-open = Nepodařilo se otevřít „{ $path }“ -error-unsupported-format = Nepodporovaný formát souboru -error-no-image-loaded = Není načten žádný obrázek - - -## Properties panel -panel-properties = Vlastnosti -panel-actions = Akce - -meta-section-file = Informace o souboru -meta-section-exif = Informace o kameře -meta-section-image = Informace o obrázku - -## File metadata -meta-filename = Název -meta-format = Formát -meta-dimensions = Rozměry -meta-filesize = Velikost -meta-colortype = Typ barev -meta-path = Cesta -meta-pages = Stránky -meta-current-page = Aktuální stránka - -## Image metadata -meta-width = Šířka -meta-height = Výška -meta-depth = Bitová hloubka - -## EXIF metadata -meta-camera = Kamera -meta-datetime = Datum pořízení -meta-exposure = Expozice -meta-aperture = Clona -meta-iso = ISO { $iso } -meta-focal = Ohnisková vzdálenost -meta-gps = Poloha GPS - -## Action buttons -action-set-wallpaper = Nastavit jako tapetu -action-open-with = Otevřít pomocí… -action-show-in-folder = Zobrazit ve složce - - -## Navigation panel (thumbnails) -nav-panel-title = Stránky -nav-panel-loading = Načítání { $current } / { $total }… diff --git a/i18n/en/noctua.ftl b/i18n/en/noctua.ftl index 3b51c8b..fb33b6a 100644 --- a/i18n/en/noctua.ftl +++ b/i18n/en/noctua.ftl @@ -33,20 +33,6 @@ menu-view-rotate-cw = Rotate Clockwise menu-view-rotate-ccw = Rotate Counter-Clockwise -## Tooltips (for buttons and icons) -tooltip-nav-previous = Previous document -tooltip-nav-next = Next document -tooltip-nav-toggle = Toggle navigation panel -tooltip-zoom-in = Zoom in -tooltip-zoom-out = Zoom out -tooltip-zoom-fit = Fit to window -tooltip-rotate-ccw = Rotate counter-clockwise -tooltip-rotate-cw = Rotate clockwise -tooltip-flip-horizontal = Flip horizontally -tooltip-flip-vertical = Flip vertically -tooltip-info-panel = Toggle info panel - - ## Footer / Status bar status-zoom-fit = Fit status-zoom-percent = { $percent }% @@ -57,6 +43,11 @@ status-separator = | ## Placeholders / Empty states no-document = No document loaded +no-document-hint = Open a document to view + +## Status / Info +status-file-position = File { $current } / { $total } +status-page-position = Page { $current } / { $total } ## Labels diff --git a/i18n/sv/noctua.ftl b/i18n/sv/noctua.ftl deleted file mode 100644 index 07fe962..0000000 --- a/i18n/sv/noctua.ftl +++ /dev/null @@ -1,127 +0,0 @@ -# SPDX-License-Identifier: GPL-3.0-or-later -# i18n/sv/noctua.ftl -# -# Localization strings for Noctua (Swedish). -# Usage: fl!("message-id", arg1, arg2, ...) -# -# Positional arguments ($1, $2, ...) are used for variable content. - - -## Program -noctua-app-name = Noctua -noctua-app-description = En dokument och bildvisare för COSMIC skrivbordet - -## Main window -window-title = { $filename -> - [none] Noctua - *[some] { $filename } — Noctua -} - - -## Menyposter -menu-main = Meny -menu-file-open = Öppna… -menu-file-quit = Avsluta -menu-view-zoom-in = Zooma in -menu-view-zoom-out = Zooma ut -menu-view-zoom-reset = Återställ zoom -menu-view-zoom-fit = Anpassa till fönster -menu-view-flip-horizontal = Vänd horisontellt -menu-view-flip-vertical = Vänd vertikalt -menu-view-rotate-cw = Rotera medurs -menu-view-rotate-ccw = Rotera moturs - - -## Verktygstips (för knappar och ikoner) -tooltip-nav-previous = Föregående dokument -tooltip-nav-next = Nästa dokument -tooltip-nav-toggle = Växla navigeringspanel -tooltip-zoom-in = Zooma in -tooltip-zoom-out = Zooma ut -tooltip-zoom-fit = Anpassa till fönster -tooltip-rotate-ccw = Rotera moturs -tooltip-rotate-cw = Rotera medurs -tooltip-flip-horizontal = Vänd horisontellt -tooltip-flip-vertical = Vänd vertikalt -tooltip-info-panel = Växla informationspanel - - -## Sidfot / Statusfält -status-zoom-fit = Passa -status-zoom-percent = { $percent }% -status-doc-dimensions = { $width } × { $height } -status-nav-position = { $current } / { $total } -status-separator = | - - -## Platshållare / Tomma tillstånd -no-document = Inget dokument laddat - - -## Etiketter -label-zoom = Zoom -label-tools = Verktyg -label-crop = Beskära -label-scale = Skala -label-page = Sida -label-pages = Sidor - - -## Laddningstillstånd -loading-metadata = Laddar metadata… -loading-thumbnails = Laddar { $current } / { $total }… - - -## Felmeddelanden -error-failed-to-open = Misslyckades att öppna "{ $path }" -error-unsupported-format = Filformat som inte stöds -error-no-image-loaded = Ingen bild laddad - - -## Egenskapspanel -panel-properties = Egenskaper -panel-actions = Åtgärder - -meta-section-file = Fil information -meta-section-exif = Kamera information -meta-section-image = Bild information - -## Fil metadata -meta-filename = Namn -meta-format = Format -meta-dimensions = Dimensioner -meta-filesize = Storlek -meta-colortype = Färg-typ -meta-path = Sökväg -meta-pages = Sidor -meta-current-page = Nuvarande sida - -## Bildmetadata -meta-width = Bredd -meta-height = Höjd -meta-depth = Bitdjup - -## EXIF metadata -meta-camera = Kamera -meta-datetime = Datum taget -meta-exposure = Exponering -meta-aperture = Bländare -meta-iso = ISO { $iso } -meta-focal = Brännvidd -meta-gps = GPS plats - -## Åtgärdsknappar -action-set-wallpaper = Använd som bakgrundsbild -action-open-with = Öppna med… -action-show-in-folder = Visa i mapp - - -## Navigeringspanel (tumnaglar) -nav-panel-title = Sidor -nav-panel-loading = Laddar { $current } / { $total }… - - -## Formatpanel -format-section-title = Pappersformat -format-section-subtitle = Välj pappersstorlek för export -orientation-section-title = Orientering diff --git a/justfile b/justfile index c45e1e1..65263c9 100644 --- a/justfile +++ b/justfile @@ -1,4 +1,4 @@ -# Name of the application's binary. +# Name of the application binary (produced by the ui crate). name := 'noctua' # The unique ID of the application. appid := 'org.codeberg.wfx.Noctua' @@ -25,7 +25,7 @@ desktop-dst := base-dir / 'share' / 'applications' / desktop icons-dst := base-dir / 'share' / 'icons' / 'hicolor' icon-svg-dst := icons-dst / 'scalable' / 'apps' / icon-svg -# Default recipe which runs `just build-release` +# Default recipe default: build-release # Runs `cargo clean` @@ -39,9 +39,9 @@ clean-vendor: # `cargo clean` and removes vendored dependencies clean-dist: clean clean-vendor -# Compiles with debug profile +# Compiles the entire workspace with debug profile build-debug *args: - cargo build {{args}} + cargo build --workspace {{args}} # Compiles with release profile build-release *args: (build-debug '--release' args) @@ -49,14 +49,18 @@ build-release *args: (build-debug '--release' args) # Compiles release profile with vendored dependencies build-vendored *args: vendor-extract (build-release '--frozen --offline' args) -# Runs a clippy check +# Runs clippy on the entire workspace check *args: - cargo clippy --all-features {{args}} -- -W clippy::pedantic + cargo clippy --workspace --all-features {{args}} -- -W clippy::pedantic -# Runs a clippy check with JSON message format +# Runs clippy with JSON message format check-json: (check '--message-format=json') -# Run the application for testing purposes +# Runs tests on the entire workspace +test *args: + cargo test --workspace {{args}} + +# Runs the application (no args = COSMIC UI, with args = CLI mode) run *args: env RUST_BACKTRACE=full cargo run --release {{args}} @@ -91,10 +95,10 @@ vendor-extract: rm -rf vendor tar pxf vendor.tar -# Bump cargo version, create git commit, and create tag +# Bump version in all crates, commit and tag tag version: find -type f -name Cargo.toml -exec sed -i '0,/^version/s/^version.*/version = "{{version}}"/' '{}' \; -exec git add '{}' \; - cargo check + cargo check --workspace cargo clean git add Cargo.lock git commit -m 'release: {{version}}' diff --git a/resources/org.codeberg.wfx.Noctua.desktop b/resources/org.codeberg.wfx.Noctua.desktop index 35d5bd9..9bb2d0a 100644 --- a/resources/org.codeberg.wfx.Noctua.desktop +++ b/resources/org.codeberg.wfx.Noctua.desktop @@ -1,14 +1,12 @@ [Desktop Entry] Name=Noctua Comment=A wise document and image viewer for the COSMIC™ desktop -Comment[cs]=Chytrý prohlížeč dokumentů a obrázku pro prostředí COSMIC™ Type=Application Icon=org.codeberg.wfx.Noctua -Exec=noctua %F +Exec=/usr/bin/noctua %F Terminal=false StartupNotify=true Categories=Graphics;Viewer;Utility; StartupWMClass=org.codeberg.wfx.Noctua Keywords=image;document;pdf;viewer;cosmic; -Keywords[cs]=obrázek;dokument;pdf;prohlížeč;fotka; -MimeType=image/png;image/jpeg;image/gif;image/webp;image/bmp;image/tiff;image/svg+xml;application/pdf; +MimeType=image/png;image/x-png;image/jpeg;image/jpg;image/pjpeg;image/gif;image/webp;image/bmp;image/x-bmp;image/tiff;image/svg+xml;application/pdf; diff --git a/resources/org.codeberg.wfx.Noctua.metainfo.xml b/resources/org.codeberg.wfx.Noctua.metainfo.xml index 95b1479..ae24682 100644 --- a/resources/org.codeberg.wfx.Noctua.metainfo.xml +++ b/resources/org.codeberg.wfx.Noctua.metainfo.xml @@ -8,17 +8,12 @@ GPL-3.0-or-later Noctua A wise document and image viewer for the COSMIC™ desktop - Chytrý prohlížeč dokumentů a obrázků pro prostředí COSMIC™

Noctua is a lightweight, keyboard-driven viewer for images, PDFs, and vector graphics. Built with Rust and libcosmic, it integrates seamlessly with COSMIC™ and GTK-based desktops.

-

- Noctua je nenáročný prohlížeč obrázků, PDF a vektorové grafiky, snadno ovladatelný pomocí klávesnice. - Je napsaný v jazyce Rust a postavený na libcosmic, díky čemuž se plynule integruje s prostředím COSMIC™ a prostředími založenými na GTK. -

org.codeberg.wfx.Noctua.desktop @@ -30,10 +25,14 @@ noctua image/png + image/x-png image/jpeg + image/jpg + image/pjpeg image/gif image/webp image/bmp + image/x-bmp image/tiff image/svg+xml application/pdf diff --git a/rustfmt.toml b/rustfmt.toml deleted file mode 100644 index 51a7648..0000000 --- a/rustfmt.toml +++ /dev/null @@ -1,5 +0,0 @@ -# rustfmt.toml -reorder_imports = false -reorder_modules = false -imports_granularity = "Preserve" -group_imports = "Preserve" diff --git a/src/active_state.rs b/src/active_state.rs new file mode 100644 index 0000000..6525747 --- /dev/null +++ b/src/active_state.rs @@ -0,0 +1,165 @@ +// SPDX-License-Identifier: GPL-3.0-or-later +// src/active_state.rs +// +// Pure navigation state: tracks which session and item are currently active. + +use anyhow::{Context, Result}; +use uuid::Uuid; + +use crate::workspace::model::{Item, Session, Workspace}; + +/// Tracks the active session and item selection. +/// +/// `ActiveState` does not own or store a `Workspace`. All methods that need +/// workspace data receive it as a parameter. This keeps navigation state +/// cleanly separated from domain data. +#[derive(Debug, Clone)] +pub struct ActiveState { + active_session: Option, + active_item: Option, +} + +impl Default for ActiveState { + fn default() -> Self { + Self::new() + } +} + +impl ActiveState { + #[must_use] + pub fn new() -> Self { + Self { + active_session: None, + active_item: None, + } + } + + /// Clear all navigation state. + pub fn reset(&mut self) { + self.active_session = None; + self.active_item = None; + } + + pub fn select_session(&mut self, workspace: &Workspace, name: &str) -> Result<()> { + let session = workspace.session_by_name(name)?; + if self.active_session == Some(session.id) { + return Ok(()); + } + self.active_session = Some(session.id); + self.active_item = if session.items.is_empty() { + None + } else { + Some(0) + }; + Ok(()) + } + + pub fn select_session_by_id(&mut self, workspace: &Workspace, id: Uuid) -> Result<()> { + if self.active_session == Some(id) { + return Ok(()); + } + let session = workspace.session(id)?; + self.active_session = Some(session.id); + self.active_item = if session.items.is_empty() { + None + } else { + Some(0) + }; + Ok(()) + } + + pub fn clear_session(&mut self) { + self.active_session = None; + self.active_item = None; + } + + pub fn select_item(&mut self, workspace: &Workspace, index: usize) -> Result<()> { + let session = self.require_active_session(workspace)?; + if index >= session.items.len() { + anyhow::bail!( + "Item index {index} out of range (session {:?} has {} items)", + session.name, + session.items.len() + ); + } + self.active_item = Some(index); + Ok(()) + } + + pub fn next_item(&mut self, workspace: &Workspace) { + let count = self.item_count(workspace); + if let Some(idx) = self.active_item { + if idx + 1 < count { + self.active_item = Some(idx + 1); + } + } + } + + pub fn previous_item(&mut self) { + if let Some(idx) = self.active_item { + if idx > 0 { + self.active_item = Some(idx - 1); + } + } + } + + /// Adjust the item cursor after an item was removed from the active session. + pub fn adjust_after_remove(&mut self, workspace: &Workspace) { + let count = self.item_count(workspace); + self.active_item = if count == 0 { + None + } else { + Some(self.active_item.unwrap_or(0).min(count - 1)) + }; + } + + /// Point the item cursor to the last item in the active session. + pub fn select_last_item(&mut self, workspace: &Workspace) { + let count = self.item_count(workspace); + self.active_item = if count == 0 { None } else { Some(count - 1) }; + } + + // -- Read-only accessors -- + + #[must_use] + pub fn active_session_id(&self) -> Option { + self.active_session + } + + #[must_use] + pub fn active_item_index(&self) -> Option { + self.active_item + } + + /// Returns the active session from the workspace. + #[must_use] + pub fn active_session<'a>(&self, workspace: &'a Workspace) -> Option<&'a Session> { + let id = self.active_session?; + workspace.session(id).ok() + } + + /// Returns the active item from the active session. + #[must_use] + pub fn active_item<'a>(&self, workspace: &'a Workspace) -> Option<&'a Item> { + let session = self.active_session(workspace)?; + let idx = self.active_item?; + session.items.get(idx) + } + + /// Returns the UUID of the active item. + #[must_use] + pub fn active_item_id(&self, workspace: &Workspace) -> Option { + self.active_item(workspace).map(|i| i.id) + } + + // Private helpers + + fn require_active_session<'a>(&self, workspace: &'a Workspace) -> Result<&'a Session> { + let id = self.active_session.context("No active session")?; + workspace.session(id) + } + + fn item_count(&self, workspace: &Workspace) -> usize { + self.active_session(workspace).map_or(0, |s| s.items.len()) + } +} diff --git a/src/application/commands/crop_document.rs b/src/application/commands/crop_document.rs deleted file mode 100644 index 2d2f408..0000000 --- a/src/application/commands/crop_document.rs +++ /dev/null @@ -1,228 +0,0 @@ -// SPDX-License-Identifier: GPL-3.0-or-later -// src/application/commands/crop_document.rs -// -// Crop document command: crop the current document to a specified region. - -use cosmic::iced::{ContentFit, Size, Vector}; - -use crate::application::DocumentManager; -use crate::domain::document::core::content::DocumentKind; -use crate::domain::document::core::document::DocResult; -use crate::domain::document::operations::CropRegion; - -/// Crop document command. -/// -/// Crops the current document to the specified rectangular region. -/// The coordinates are in image pixels (not canvas/screen coordinates). -pub struct CropDocumentCommand { - /// X coordinate of the crop region (top-left corner). - pub x: u32, - /// Y coordinate of the crop region (top-left corner). - pub y: u32, - /// Width of the crop region in pixels. - pub width: u32, - /// Height of the crop region in pixels. - pub height: u32, -} - -impl CropDocumentCommand { - /// Create a new crop document command. - #[must_use] - pub fn new(x: u32, y: u32, width: u32, height: u32) -> Self { - Self { - x, - y, - width, - height, - } - } - - /// Create a crop command from canvas coordinates. - /// - /// Converts canvas-space coordinates to image-space pixels based on - /// the current view state (scale, pan, content fit). - /// - /// # Errors - /// - /// Returns an error if the crop region is invalid or outside image bounds. - pub fn from_canvas_selection( - crop_region: &CropRegion, - canvas_size: Size, - image_size: Size, - scale: f32, - pan_offset: Vector, - ) -> Result { - let (x, y, w, h) = crop_region.as_tuple(); - let canvas_rect = (x as f32, y as f32, w as f32, h as f32); - - // Convert canvas coordinates to image pixel coordinates - let image_rect = Self::canvas_rect_to_image_rect( - canvas_rect, - canvas_size, - image_size, - scale, - pan_offset, - ContentFit::Contain, - ) - .ok_or_else(|| "Invalid crop region".to_string())?; - - Ok(Self { - x: image_rect.0, - y: image_rect.1, - width: image_rect.2, - height: image_rect.3, - }) - } - - /// Convert canvas rectangle to image pixel rectangle. - /// - /// This is the core coordinate transformation logic that maps from - /// canvas/screen coordinates to actual image pixel coordinates. - fn canvas_rect_to_image_rect( - canvas_rect: (f32, f32, f32, f32), - canvas_size: Size, - image_size: Size, - scale: f32, - offset: Vector, - content_fit: ContentFit, - ) -> Option<(u32, u32, u32, u32)> { - let (cx, cy, cw, ch) = canvas_rect; - - if cw <= 1.0 || ch <= 1.0 { - return None; - } - - // Transform top-left and bottom-right corners - let (x1, y1) = Self::canvas_to_image_coords( - cx, - cy, - canvas_size, - image_size, - scale, - offset, - content_fit, - ); - let (x2, y2) = Self::canvas_to_image_coords( - cx + cw, - cy + ch, - canvas_size, - image_size, - scale, - offset, - content_fit, - ); - - // Clamp to image boundaries - let img_x = x1.max(0.0).min(image_size.width); - let img_y = y1.max(0.0).min(image_size.height); - let img_w = (x2 - x1).max(1.0).min(image_size.width - img_x); - let img_h = (y2 - y1).max(1.0).min(image_size.height - img_y); - - #[allow(clippy::cast_possible_truncation, clippy::cast_sign_loss)] - Some(( - img_x.round() as u32, - img_y.round() as u32, - img_w.round() as u32, - img_h.round() as u32, - )) - } - - /// Convert a single point from canvas coordinates to image coordinates. - fn canvas_to_image_coords( - cx: f32, - cy: f32, - canvas_size: Size, - image_size: Size, - scale: f32, - offset: Vector, - content_fit: ContentFit, - ) -> (f32, f32) { - // Calculate displayed image dimensions based on ContentFit - let (display_w, display_h) = match content_fit { - ContentFit::Contain => { - let aspect = image_size.width / image_size.height; - let canvas_aspect = canvas_size.width / canvas_size.height; - - if aspect > canvas_aspect { - // Limited by width - (canvas_size.width, canvas_size.width / aspect) - } else { - // Limited by height - (canvas_size.height * aspect, canvas_size.height) - } - } - _ => (image_size.width, image_size.height), - }; - - // Apply scale - let scaled_w = display_w * scale; - let scaled_h = display_h * scale; - - // Center in canvas - let center_x = (canvas_size.width - scaled_w) / 2.0; - let center_y = (canvas_size.height - scaled_h) / 2.0; - - // Convert canvas coords to scaled image coords - let img_x = (cx - center_x - offset.x) / scale; - let img_y = (cy - center_y - offset.y) / scale; - - // Scale from display space to actual image pixel space - let pixel_x = (img_x / display_w) * image_size.width; - let pixel_y = (img_y / display_h) * image_size.height; - - (pixel_x, pixel_y) - } - - /// Execute the crop command on the document manager. - /// - /// # Errors - /// - /// Returns an error if: - /// - No document is currently open - /// - The document type doesn't support cropping - /// - The crop region is invalid - /// - The crop operation fails - pub fn execute(&self, manager: &mut DocumentManager) -> DocResult<()> { - let doc = manager - .current_document_mut() - .ok_or_else(|| anyhow::anyhow!("No document open"))?; - - // Only raster images support cropping - if doc.kind() != DocumentKind::Raster { - return Err(anyhow::anyhow!( - "Crop operation is only supported for raster images" - )); - } - - // Get the raster document and apply crop - if let crate::domain::document::core::content::DocumentContent::Raster(raster) = doc { - raster - .crop(self.x, self.y, self.width, self.height) - .map_err(|e| anyhow::anyhow!("Crop failed: {}", e))?; - } - - Ok(()) - } - - /// Check if the command can be executed. - #[must_use] - pub fn can_execute(&self, manager: &DocumentManager) -> bool { - manager - .current_document() - .map_or(false, |doc| doc.kind() == DocumentKind::Raster) - } -} - -#[cfg(test)] -mod tests { - use super::*; - - #[test] - fn test_command_creation() { - let cmd = CropDocumentCommand::new(10, 20, 100, 150); - assert_eq!(cmd.x, 10); - assert_eq!(cmd.y, 20); - assert_eq!(cmd.width, 100); - assert_eq!(cmd.height, 150); - } -} diff --git a/src/application/commands/mod.rs b/src/application/commands/mod.rs deleted file mode 100644 index c6ab7bd..0000000 --- a/src/application/commands/mod.rs +++ /dev/null @@ -1,10 +0,0 @@ -// SPDX-License-Identifier: GPL-3.0-or-later -// src/application/commands/mod.rs -// -// Application commands: document operations and navigation. - -pub mod crop_document; -pub mod navigate; -pub mod open_document; -pub mod save_document; -pub mod transform_document; diff --git a/src/application/commands/navigate.rs b/src/application/commands/navigate.rs deleted file mode 100644 index c46cac3..0000000 --- a/src/application/commands/navigate.rs +++ /dev/null @@ -1,67 +0,0 @@ -// SPDX-License-Identifier: GPL-3.0-or-later -// src/application/commands/navigate.rs -// -// Navigation command: next/previous document. -// Reserved for future CQRS pattern - currently using direct DocumentManager methods. - -#![allow(dead_code)] - -use std::path::PathBuf; - -use crate::application::document_manager::DocumentManager; -use crate::domain::document::core::document::DocResult; - -/// Navigation direction. -#[derive(Debug, Clone, Copy, PartialEq, Eq)] -pub enum NavigationDirection { - /// Navigate to next document. - Next, - /// Navigate to previous document. - Previous, -} - -/// Navigate command. -pub struct NavigateCommand { - direction: NavigationDirection, -} - -impl NavigateCommand { - /// Create a new navigate command. - #[must_use] - pub fn new(direction: NavigationDirection) -> Self { - Self { direction } - } - - /// Execute the navigate command. - pub fn execute(&self, manager: &mut DocumentManager) -> DocResult> { - let path = match self.direction { - NavigationDirection::Next => manager.next_document(), - NavigationDirection::Previous => manager.previous_document(), - }; - - Ok(path) - } - - /// Check if navigation is possible. - #[must_use] - pub fn can_execute(&self, manager: &DocumentManager) -> bool { - match self.direction { - NavigationDirection::Next => manager.has_next(), - NavigationDirection::Previous => manager.has_previous(), - } - } -} - -#[cfg(test)] -mod tests { - use super::*; - - #[test] - fn test_navigate_command_creation() { - let cmd = NavigateCommand::new(NavigationDirection::Next); - assert_eq!(cmd.direction, NavigationDirection::Next); - - let cmd = NavigateCommand::new(NavigationDirection::Previous); - assert_eq!(cmd.direction, NavigationDirection::Previous); - } -} diff --git a/src/application/commands/open_document.rs b/src/application/commands/open_document.rs deleted file mode 100644 index 35fab80..0000000 --- a/src/application/commands/open_document.rs +++ /dev/null @@ -1,34 +0,0 @@ -// SPDX-License-Identifier: GPL-3.0-or-later -// src/application/commands/open_document.rs -// -// Open document command: load a document from a file path. -// Reserved for future CQRS pattern - currently using direct DocumentManager methods. - -#![allow(dead_code)] - -use std::path::Path; - -use crate::application::document_manager::DocumentManager; -use crate::domain::document::core::document::DocResult; - -/// Open document command. -pub struct OpenDocumentCommand; - -impl OpenDocumentCommand { - /// Create a new open document command. - #[must_use] - pub fn new() -> Self { - Self - } - - /// Execute the open document command. - pub fn execute(&self, manager: &mut DocumentManager, path: &Path) -> DocResult<()> { - manager.open_document(path) - } -} - -impl Default for OpenDocumentCommand { - fn default() -> Self { - Self::new() - } -} diff --git a/src/application/commands/save_document.rs b/src/application/commands/save_document.rs deleted file mode 100644 index 9761ab1..0000000 --- a/src/application/commands/save_document.rs +++ /dev/null @@ -1,64 +0,0 @@ -// SPDX-License-Identifier: GPL-3.0-or-later -// src/application/commands/save_document.rs -// -// Save document command: export document to a file. -// Reserved for future implementation - not yet used. - -#![allow(dead_code)] - -use std::path::Path; - -use crate::application::document_manager::DocumentManager; -use crate::domain::document::core::document::DocResult; -use crate::domain::document::operations::export::ExportFormat; - -/// Save document command. -pub struct SaveDocumentCommand { - /// Target format for export. - format: Option, -} - -impl SaveDocumentCommand { - /// Create a new save document command with automatic format detection. - #[must_use] - pub fn new() -> Self { - Self { format: None } - } - - /// Create a save document command with a specific format. - #[must_use] - pub fn with_format(format: ExportFormat) -> Self { - Self { - format: Some(format), - } - } - - /// Execute the save document command. - pub fn execute(&self, manager: &DocumentManager, path: &Path) -> DocResult<()> { - let _document = manager - .current_document() - .ok_or_else(|| anyhow::anyhow!("No document loaded"))?; - - // Detect format from path or use specified format - let format = self - .format - .or_else(|| ExportFormat::from_path(path)) - .ok_or_else(|| anyhow::anyhow!("Could not determine export format"))?; - - // TODO: Implement actual save logic - // This would involve: - // 1. Getting the rendered image from the document - // 2. Applying any necessary transformations - // 3. Exporting to the target format - - log::info!("Save to {} as {:?}", path.display(), format); - - Err(anyhow::anyhow!("Save operation not yet implemented")) - } -} - -impl Default for SaveDocumentCommand { - fn default() -> Self { - Self::new() - } -} diff --git a/src/application/commands/transform_document.rs b/src/application/commands/transform_document.rs deleted file mode 100644 index d2f5bc4..0000000 --- a/src/application/commands/transform_document.rs +++ /dev/null @@ -1,81 +0,0 @@ -// SPDX-License-Identifier: GPL-3.0-or-later -// src/application/commands/transform_document.rs -// -// Transform document command: rotate, flip, and other transformations. - -use crate::application::document_manager::DocumentManager; -use crate::domain::document::core::document::{DocResult, Rotation}; -use crate::domain::document::operations::transform; - -/// Transformation operation. -#[derive(Debug, Clone, Copy, PartialEq, Eq)] -#[allow(dead_code)] -pub enum TransformOperation { - /// Rotate clockwise by 90 degrees. - RotateCw, - /// Rotate counter-clockwise by 90 degrees. - RotateCcw, - /// Flip horizontally. - FlipHorizontal, - /// Flip vertically. - FlipVertical, - /// Rotate to a specific angle. - RotateTo(Rotation), -} - -/// Transform document command. -pub struct TransformDocumentCommand { - operation: TransformOperation, -} - -impl TransformDocumentCommand { - /// Create a new transform document command. - #[must_use] - pub fn new(operation: TransformOperation) -> Self { - Self { operation } - } - - /// Execute the transform command. - /// - /// Uses high-level transform operations that work across all document types - /// (Raster, Vector, Portable). - pub fn execute(&self, manager: &mut DocumentManager) -> DocResult<()> { - let document = manager - .current_document_mut() - .ok_or_else(|| anyhow::anyhow!("No document loaded"))?; - - match self.operation { - TransformOperation::RotateCw => { - transform::rotate_document_cw(document)?; - } - TransformOperation::RotateCcw => { - transform::rotate_document_ccw(document)?; - } - TransformOperation::FlipHorizontal => { - transform::flip_document_horizontal(document)?; - } - TransformOperation::FlipVertical => { - transform::flip_document_vertical(document)?; - } - TransformOperation::RotateTo(rotation) => { - transform::rotate_document_to(document, rotation)?; - } - } - - Ok(()) - } -} - -#[cfg(test)] -mod tests { - use super::*; - - #[test] - fn test_transform_command_creation() { - let cmd = TransformDocumentCommand::new(TransformOperation::RotateCw); - assert_eq!(cmd.operation, TransformOperation::RotateCw); - - let cmd = TransformDocumentCommand::new(TransformOperation::FlipHorizontal); - assert_eq!(cmd.operation, TransformOperation::FlipHorizontal); - } -} diff --git a/src/application/document_manager.rs b/src/application/document_manager.rs deleted file mode 100644 index 29f9f8b..0000000 --- a/src/application/document_manager.rs +++ /dev/null @@ -1,216 +0,0 @@ -// SPDX-License-Identifier: GPL-3.0-or-later -// src/application/document_manager.rs -// -// Document manager: orchestrates document lifecycle and navigation. - -use std::path::{Path, PathBuf}; - -use crate::domain::document::collection::DocumentCollection; -use crate::domain::document::core::content::DocumentContent; -use crate::domain::document::core::document::DocResult; -use crate::domain::document::core::metadata::DocumentMeta; -use crate::infrastructure::filesystem::file_ops; -use crate::infrastructure::loaders::DocumentLoaderFactory; - -/// Central document manager. -/// -/// Orchestrates document loading, metadata extraction, and folder navigation. -/// Uses DocumentCollection (Domain Layer) for navigation logic. -pub struct DocumentManager { - /// Document collection for navigation (Domain Layer abstraction). - collection: DocumentCollection, - /// Current document metadata. - current_metadata: Option, - /// Document loader factory. - loader: DocumentLoaderFactory, -} - -impl DocumentManager { - /// Create a new document manager. - #[must_use] - pub fn new() -> Self { - Self { - collection: DocumentCollection::new(), - current_metadata: None, - loader: DocumentLoaderFactory::new(), - } - } - - /// Open a document from a file path or directory. - /// - /// If a directory is provided, opens the first supported file found. - /// Also scans the parent folder for navigation. - pub fn open_document(&mut self, path: &Path) -> DocResult<()> { - // Determine the actual file to open - let file_path = if path.is_dir() { - // Scan directory and find first supported file - let paths = file_ops::collect_supported_files(path); - self.collection = DocumentCollection::from_paths(paths); - - self.collection - .current_path() - .ok_or_else(|| anyhow::anyhow!("No supported files found in directory"))? - .clone() - } else { - path.to_path_buf() - }; - - // Load the document - let document = self.loader.load(&file_path)?; - - // Extract metadata - let metadata = self.extract_metadata(&file_path, &document); - - // Scan folder for navigation if not already done - if !path.is_dir() { - if let Some(parent) = file_path.parent() { - let paths = file_ops::collect_supported_files(parent); - self.collection = DocumentCollection::from_paths(paths); - // Find and set current document index - if let Some(idx) = self.collection.paths().iter().position(|p| p == &file_path) { - self.collection.goto(idx); - } - } - } - - // Generate thumbnails for multi-page documents (PDF) - let mut document = document; - if document.is_multi_page() { - log::info!("Generating thumbnails for multi-page document..."); - if let Err(e) = document.generate_thumbnails() { - log::warn!("Failed to generate thumbnails: {e}"); - } - } - - // Store document in collection - self.collection.set_current_document(document); - self.current_metadata = Some(metadata); - - Ok(()) - } - - /// Get the current document. - #[must_use] - pub fn current_document(&self) -> Option<&DocumentContent> { - self.collection.current_document() - } - - /// Get a mutable reference to the current document. - #[must_use] - pub fn current_document_mut(&mut self) -> Option<&mut DocumentContent> { - self.collection.current_document_mut() - } - - /// Get thumbnail handle for a specific page (read-only access). - /// Returns None if the thumbnail hasn't been generated yet. - #[must_use] - pub fn get_thumbnail_handle(&self, page: usize) -> Option { - self.collection - .current_document()? - .get_thumbnail_handle(page) - } - - /// Get the current document path. - #[must_use] - pub fn current_path(&self) -> Option<&Path> { - self.collection.current_path().map(|p| p.as_path()) - } - - /// Get the current document metadata. - #[must_use] - pub fn current_metadata(&self) -> Option<&DocumentMeta> { - self.current_metadata.as_ref() - } - - /// Get all folder entries for navigation. - #[must_use] - pub fn folder_entries(&self) -> &[PathBuf] { - self.collection.paths() - } - - /// Get current index in folder. - #[must_use] - pub fn current_index(&self) -> Option { - self.collection.current_index() - } - - /// Navigate to the next document in the folder. - /// - /// Wraps around to the first document when at the end. - pub fn next_document(&mut self) -> Option { - // Use DocumentCollection navigation - if self.collection.has_next() { - self.collection.next(); - } else if !self.collection.is_empty() { - // Wrap around to first - self.collection.goto(0); - } else { - return None; - } - - let next_path = self.collection.current_path()?.clone(); - if self.open_document(&next_path).is_ok() { - Some(next_path) - } else { - None - } - } - - /// Navigate to the previous document in the folder. - /// - /// Wraps around to the last document when at the beginning. - pub fn previous_document(&mut self) -> Option { - // Use DocumentCollection navigation - if self.collection.has_previous() { - self.collection.previous(); - } else if !self.collection.is_empty() { - // Wrap around to last - let last_idx = self.collection.len() - 1; - self.collection.goto(last_idx); - } else { - return None; - } - - let prev_path = self.collection.current_path()?.clone(); - if self.open_document(&prev_path).is_ok() { - Some(prev_path) - } else { - None - } - } - - /// Close the current document. - #[allow(dead_code)] - pub fn close_document(&mut self) { - self.collection.clear_current_document(); - self.current_metadata = None; - } - - /// Extract metadata from a document. - fn extract_metadata(&self, path: &Path, document: &DocumentContent) -> DocumentMeta { - // Use the document's own extract_meta() method - // This properly delegates to the type-specific implementation - // (RasterDocument, VectorDocument, or PortableDocument) - document.extract_meta(path) - } - - /// Check if there is a next document available. - #[must_use] - #[allow(dead_code)] - pub fn has_next(&self) -> bool { - self.collection.has_next() - } - - /// Check if there is a previous document available. - #[must_use] - #[allow(dead_code)] - pub fn has_previous(&self) -> bool { - self.collection.has_previous() - } -} - -impl Default for DocumentManager { - fn default() -> Self { - Self::new() - } -} diff --git a/src/application/mod.rs b/src/application/mod.rs deleted file mode 100644 index 00e19f2..0000000 --- a/src/application/mod.rs +++ /dev/null @@ -1,11 +0,0 @@ -// SPDX-License-Identifier: GPL-3.0-or-later -// src/application/mod.rs -// -// Application layer: use cases, commands, queries, and services. - -pub mod commands; -pub mod document_manager; -pub mod services; - -// Re-export document manager -pub use document_manager::DocumentManager; diff --git a/src/application/services/cache_service.rs b/src/application/services/cache_service.rs deleted file mode 100644 index 2fda4bc..0000000 --- a/src/application/services/cache_service.rs +++ /dev/null @@ -1,81 +0,0 @@ -// SPDX-License-Identifier: GPL-3.0-or-later -// src/application/services/cache_service.rs -// -// Cache service: manages document and thumbnail caching. -// Reserved for future caching layer implementation. - -#![allow(dead_code)] - -use std::path::Path; - -use cosmic::widget::image::Handle as ImageHandle; -use image::DynamicImage; - -use crate::infrastructure::cache::ThumbnailCache; - -/// Cache service for managing document caches. -/// -/// Provides high-level caching operations for the application layer. -pub struct CacheService; - -impl CacheService { - /// Create a new cache service. - #[must_use] - pub fn new() -> Self { - Self - } - - /// Load a thumbnail from cache. - /// - /// Returns None if the thumbnail is not cached or the cache is invalid. - #[must_use] - pub fn get_thumbnail(&self, path: &Path, page: usize) -> Option { - ThumbnailCache::load(path, page) - } - - /// Save a thumbnail to cache. - /// - /// Returns true if the thumbnail was successfully cached. - pub fn put_thumbnail(&self, path: &Path, page: usize, image: &DynamicImage) -> bool { - ThumbnailCache::save(path, page, image).is_some() - } - - /// Clear all cached thumbnails. - /// - /// This operation is not yet implemented. - pub fn clear_cache(&self) -> Result<(), String> { - ThumbnailCache::clear_cache().map_err(|e| e.to_string()) - } - - /// Get the size of the cache directory. - /// - /// Returns the total size in bytes, or None if it cannot be determined. - #[must_use] - pub fn cache_size(&self) -> Option { - // TODO: Implement cache size calculation - None - } -} - -impl Default for CacheService { - fn default() -> Self { - Self::new() - } -} - -#[cfg(test)] -mod tests { - use super::*; - - #[test] - fn test_cache_service_creation() { - let service = CacheService::new(); - assert!(std::ptr::eq(&service, &service)); // Dummy test - } - - #[test] - fn test_cache_service_default() { - let service = CacheService::default(); - assert!(std::ptr::eq(&service, &service)); // Dummy test - } -} diff --git a/src/application/services/mod.rs b/src/application/services/mod.rs deleted file mode 100644 index 8efbbe9..0000000 --- a/src/application/services/mod.rs +++ /dev/null @@ -1,7 +0,0 @@ -// SPDX-License-Identifier: GPL-3.0-or-later -// src/application/services/mod.rs -// -// Application services: cache management and preview generation. - -pub mod cache_service; -pub mod preview_service; diff --git a/src/application/services/preview_service.rs b/src/application/services/preview_service.rs deleted file mode 100644 index ab23906..0000000 --- a/src/application/services/preview_service.rs +++ /dev/null @@ -1,119 +0,0 @@ -// SPDX-License-Identifier: GPL-3.0-or-later -// src/application/services/preview_service.rs -// -// Preview service: generates thumbnails and previews for documents. -// Reserved for future async thumbnail generation implementation. - -#![allow(dead_code)] - -use cosmic::widget::image::Handle as ImageHandle; - -use crate::domain::document::core::content::DocumentContent; -use crate::domain::document::core::document::DocResult; - -/// Preview service for generating document thumbnails and previews. -/// -/// Provides high-level preview generation operations for the application layer. -pub struct PreviewService { - /// Target thumbnail size (width in pixels). - thumbnail_size: u32, -} - -impl PreviewService { - /// Create a new preview service with default thumbnail size. - #[must_use] - pub fn new() -> Self { - Self { - thumbnail_size: 256, - } - } - - /// Create a preview service with a specific thumbnail size. - #[must_use] - pub fn with_thumbnail_size(size: u32) -> Self { - Self { - thumbnail_size: size, - } - } - - /// Set the thumbnail size. - pub fn set_thumbnail_size(&mut self, size: u32) { - self.thumbnail_size = size; - } - - /// Get the current thumbnail size. - #[must_use] - pub fn thumbnail_size(&self) -> u32 { - self.thumbnail_size - } - - /// Generate a thumbnail for a document page. - /// - /// For single-page documents, the page parameter is ignored. - pub fn generate_thumbnail( - &self, - document: &mut DocumentContent, - page: usize, - ) -> DocResult> { - if document.is_multi_page() { - document.get_thumbnail(page) - } else { - // For single-page documents, return the current handle - Ok(document.handle()) - } - } - - /// Generate all thumbnails for a multi-page document. - /// - /// Returns the number of thumbnails generated. - pub fn generate_all_thumbnails(&self, document: &mut DocumentContent) -> DocResult { - if !document.is_multi_page() { - return Ok(0); - } - - document.generate_thumbnails()?; - Ok(document.thumbnails_loaded()) - } - - /// Check if all thumbnails are ready for a document. - #[must_use] - pub fn thumbnails_ready(&self, document: &DocumentContent) -> bool { - document.thumbnails_ready() - } - - /// Get the number of thumbnails loaded for a document. - #[must_use] - pub fn thumbnails_loaded(&self, document: &DocumentContent) -> usize { - document.thumbnails_loaded() - } -} - -impl Default for PreviewService { - fn default() -> Self { - Self::new() - } -} - -#[cfg(test)] -mod tests { - use super::*; - - #[test] - fn test_preview_service_creation() { - let service = PreviewService::new(); - assert_eq!(service.thumbnail_size(), 256); - } - - #[test] - fn test_preview_service_with_size() { - let service = PreviewService::with_thumbnail_size(512); - assert_eq!(service.thumbnail_size(), 512); - } - - #[test] - fn test_set_thumbnail_size() { - let mut service = PreviewService::new(); - service.set_thumbnail_size(128); - assert_eq!(service.thumbnail_size(), 128); - } -} diff --git a/src/config.rs b/src/config.rs deleted file mode 100644 index adcf9eb..0000000 --- a/src/config.rs +++ /dev/null @@ -1,44 +0,0 @@ -// SPDX-License-Identifier: GPL-3.0-or-later -// src/config.rs -// -// Global configuration for the application with cosmic-config support. - -use cosmic::cosmic_config::{self, CosmicConfigEntry, cosmic_config_derive::CosmicConfigEntry}; -use std::path::PathBuf; - -/// Global configuration for the application. -#[derive(Debug, Clone, CosmicConfigEntry, PartialEq)] -#[version = 1] -pub struct AppConfig { - /// Default directory to open when browsing for documents. - pub default_image_dir: Option, - /// Show page navigation panel (left sidebar for multi-page documents). - pub nav_bar_visible: bool, - /// Show properties panel (right sidebar with metadata). - pub context_drawer_visible: bool, - /// Zoom step multiplier for keyboard shortcuts (1.1 = 10% increase per step). - pub scale_step: f32, - /// Pan distance in pixels per arrow key press. - pub pan_step: f32, - /// Minimum zoom level (0.1 = 10% of original size). - pub min_scale: f32, - /// Maximum zoom level (8.0 = 800% of original size). - pub max_scale: f32, - /// Show 3x3 grid during crop selection. - pub crop_show_grid: bool, -} - -impl Default for AppConfig { - fn default() -> Self { - Self { - default_image_dir: dirs::picture_dir().or_else(dirs::home_dir), - nav_bar_visible: false, - context_drawer_visible: false, - scale_step: 1.1, - pan_step: 50.0, - min_scale: 0.1, - max_scale: 8.0, - crop_show_grid: true, - } - } -} diff --git a/src/document/command.rs b/src/document/command.rs new file mode 100644 index 0000000..7fe0ee1 --- /dev/null +++ b/src/document/command.rs @@ -0,0 +1,13 @@ +// SPDX-License-Identifier: GPL-3.0-or-later +// src/document/command.rs +// +// Commands that operate on the active document itself. + +/// Commands operating on the active document (zoom, export, annotate, …). +/// +/// The `DocumentManager` dispatches these after resolving which document is +/// active – the UI never needs to know the underlying format. +#[derive(Debug, Clone)] +pub enum DocumentCommand { + // Reserved for future use: Rotate, Flip, Zoom, Export, Annotate, … +} diff --git a/src/document/loader.rs b/src/document/loader.rs new file mode 100644 index 0000000..f361d0b --- /dev/null +++ b/src/document/loader.rs @@ -0,0 +1,101 @@ +// SPDX-License-Identifier: GPL-3.0-or-later +// src/document/loader.rs +// +// Document loading: format detection, format dispatch, and directory scanning. + +use crate::document::model::{PortableModel, RasterModel, VectorModel}; +use crate::document::session::data::{SessionData, SessionItem}; +use crate::document::types::PageLayout; +use crate::document::DocumentContent; +use crate::error::{Error, Result}; +use std::path::Path; + +/// The three document formats Noctua understands. +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +pub(crate) enum DocumentFormat { + /// Raster images – anything the image crate can decode. + Raster, + /// Vector graphics (SVG). + Vector, + /// Portable documents (PDF). + Portable, +} + +/// Detects the document format from the file extension. +/// +/// Returns `None` for unknown extensions so directory scans can skip +/// unsupported files early. At load time the underlying library +/// (`ImageReader`, `usvg`, `pdfium`) is the final authority. +fn detect_format(path: &Path) -> Option { + match path + .extension() + .and_then(|e| e.to_str()) + .map(str::to_lowercase) + .as_deref() + { + Some("pdf") => Some(DocumentFormat::Portable), + Some("svg") => Some(DocumentFormat::Vector), + Some("png" | "jpg" | "jpeg" | "webp" | "gif" | "bmp" | "tiff" | "tif") => { + Some(DocumentFormat::Raster) + } + _ => None, + } +} + +/// Loads a document from a path and returns its content. +/// +/// # Errors +/// +/// Returns [`Error::NotFound`] if the path does not exist. +/// Returns [`Error::Unsupported`] if the file extension is not recognised. +pub(crate) fn load_document(path: &Path) -> Result { + if !path.exists() { + return Err(Error::NotFound(format!("{}", path.display()))); + } + + match detect_format(path) { + Some(DocumentFormat::Portable) => { + // TODO: Replace with actual pdfium-render loading. + Ok(DocumentContent::Portable(PortableModel { + path: path.to_path_buf(), + page_count: 3, + layout: PageLayout::new(vec![(595.0, 842.0); 3], 10.0), + })) + } + Some(DocumentFormat::Vector) => { + // TODO: Replace with actual usvg parsing. + Ok(DocumentContent::Vector(VectorModel { + path: path.to_path_buf(), + view_box: None, + })) + } + Some(DocumentFormat::Raster) => { + // TODO: Replace with actual image-crate loading. + Ok(DocumentContent::Raster(RasterModel { + path: path.to_path_buf(), + dimensions: (1920, 1080), + })) + } + None => Err(Error::Unsupported(format!( + "extension: {}", + path.extension() + .and_then(|e| e.to_str()) + .unwrap_or("(none)") + ))), + } +} + +/// Scans a directory for supported files and populates the session. +pub(crate) fn scan_directory_into(session: &mut SessionData, dir: &Path) { + if let Ok(entries) = std::fs::read_dir(dir) { + let mut paths: Vec<_> = entries + .filter_map(std::result::Result::ok) + .map(|e| e.path()) + .filter(|p| p.is_file() && detect_format(p).is_some()) + .collect(); + + paths.sort(); + + session.items = paths.into_iter().map(|p| SessionItem::new(p, 0)).collect(); + } +} diff --git a/src/document/manager.rs b/src/document/manager.rs new file mode 100644 index 0000000..19de638 --- /dev/null +++ b/src/document/manager.rs @@ -0,0 +1,418 @@ +// SPDX-License-Identifier: GPL-3.0-or-later +// src/document/manager.rs +// +// Document manager: session state, cursors, clipboard, and command dispatch. + +use crate::document::loader::{load_document, scan_directory_into}; +use crate::document::session::command::SessionCommand; +use crate::document::session::data::{CollectionKind, SessionData, SessionItem}; +use crate::document::session::store; +use crate::document::DocumentContent; +use crate::error::Error; +use std::path::{Path, PathBuf}; + +/// The load state of the currently viewed document. +#[derive(Debug)] +pub enum DocumentState { + Empty, + Loading, + Loaded(DocumentContent), + Error(Error), +} + +/// Central state for all sessions, cursors, and the document clipboard. +/// +/// Two cursors are maintained: +/// - `active_session_index` – the currently active session +/// - `active_document_index` – the currently active document within that session +/// +/// All `SessionCommand` variants that do not carry an explicit target operate +/// on the active cursor. `SelectSession` and `SelectDocument` set them. +pub struct DocumentManager { + sessions: Vec, + active_session_index: Option, + active_document_index: Option, + clipboard: Option, + session_file_path: Option, + pub state: DocumentState, +} + +impl Default for DocumentManager { + fn default() -> Self { + Self { + sessions: Vec::new(), + active_session_index: None, + active_document_index: None, + clipboard: None, + session_file_path: None, + state: DocumentState::Empty, + } + } +} + +impl DocumentManager { + #[must_use] + pub fn new() -> Self { + Self::default() + } + + /// The single public entry point for all session and document state changes. + pub fn handle(&mut self, command: SessionCommand) { + match command { + // Session lifecycle + SessionCommand::NewSession { name } => self.new_session(&name), + SessionCommand::OpenSession { path } => self.open_session_file(&path), + SessionCommand::DeleteSession => self.delete_session(), + SessionCommand::SaveSession => self.save_session(), + SessionCommand::SelectSession { name } => self.select_session(&name), + SessionCommand::DuplicateSession { new_name } => self.duplicate_session(&new_name), + SessionCommand::RenameSession { name } => self.rename_session(&name), + + // Collection management + SessionCommand::AddDirectoryToSession { dir } => { + if let Some(session) = self.active_session_mut() { + scan_directory_into(session, &dir); + } + self.reset_document_cursor(); + } + SessionCommand::AddDocumentToSession { path } => self.add_document(&path), + + // Document management + SessionCommand::SelectDocument { index } => self.select_document(index), + SessionCommand::CopyDocument => self.copy_document(), + SessionCommand::PasteDocument => self.paste_document(), + SessionCommand::RemoveDocument => self.remove_document(), + SessionCommand::NextDocument => self.navigate_next(), + SessionCommand::PreviousDocument => self.navigate_previous(), + SessionCommand::MoveDocumentUp => self.move_document_up(), + SessionCommand::MoveDocumentDown => self.move_document_down(), + SessionCommand::MoveDocumentTo { index } => self.move_document_to(index), + } + } + + /// Read-only access to the active session. + #[must_use] + pub fn active_session(&self) -> Option<&SessionData> { + self.active_session_index.map(|i| &self.sessions[i]) + } + + /// The index of the currently active document within the active session. + #[must_use] + pub fn active_document_index(&self) -> Option { + self.active_document_index + } + + /// Read-only access to all sessions. + #[must_use] + pub fn sessions(&self) -> &[SessionData] { + &self.sessions + } + + /// Read-only access to a session by name. + #[must_use] + pub fn session_by_name(&self, name: &str) -> Option<&SessionData> { + self.sessions.iter().find(|s| s.name == name) + } + + // -- Private helpers -- + + fn active_session_mut(&mut self) -> Option<&mut SessionData> { + self.active_session_index.map(|i| &mut self.sessions[i]) + } + + fn index_by_name(&self, name: &str) -> Option { + self.sessions.iter().position(|s| s.name == name) + } + + /// Sets `active_document_index` to `Some(0)` if the active session has items, else `None`. + fn reset_document_cursor(&mut self) { + self.active_document_index = match self.active_session() { + Some(s) if !s.items.is_empty() => Some(0), + _ => None, + }; + } + + fn new_session(&mut self, name: &str) { + if self.index_by_name(name).is_some() { + return; + } + self.sessions.push(SessionData::new( + name, + CollectionKind::DocumentCollection, + None, + Vec::new(), + )); + } + + fn open_session_file(&mut self, path: &Path) { + match store::load_sessions(path) { + Ok(mut loaded) => { + for session in &mut loaded { + session.items.retain(|item| { + if item.path.exists() { + true + } else { + log::error!( + "Session '{}': {}", + session.name, + Error::NotFound(item.path.display().to_string()) + ); + false + } + }); + } + + let browser_index = loaded + .iter() + .position(|s| s.kind == CollectionKind::DirectoryBrowser); + + self.sessions = loaded; + self.active_session_index = browser_index; + self.session_file_path = Some(path.to_path_buf()); + self.state = DocumentState::Empty; + self.reset_document_cursor(); + } + Err(e) => { + self.state = DocumentState::Error(e); + } + } + } + + // We have no 'Close session' it wouldbe only removes it from runtime, keeps it in .ron file + // For actual deletion, use delete_session() + // So 'close_session' from ui view is save_session and close session view. + + fn delete_session(&mut self) { + let Some(idx) = self.active_session_index else { + return; + }; + + // The DirectoryBrowser session is managed by the UI and cannot be deleted. + if self.sessions[idx].kind == CollectionKind::DirectoryBrowser { + return; + } + + self.sessions.remove(idx); + self.active_document_index = None; + + self.active_session_index = if self.sessions.is_empty() { + None + } else { + Some(idx.saturating_sub(1)) + }; + } + + fn save_session(&mut self) { + let Some(ref path) = self.session_file_path else { + return; + }; + if let Err(e) = store::save_sessions(&path, &self.sessions) { + self.state = DocumentState::Error(e); + } + } + + fn select_session(&mut self, name: &str) { + if let Some(idx) = self.index_by_name(name) { + self.active_session_index = Some(idx); + self.reset_document_cursor(); + } + } + + fn duplicate_session(&mut self, new_name: &str) { + if self.index_by_name(new_name).is_some() { + return; + } + let Some(idx) = self.active_session_index else { + return; + }; + let items = self.sessions[idx].items.clone(); + self.sessions.push(SessionData::new( + new_name, + CollectionKind::DocumentCollection, + None, + items, + )); + } + + fn rename_session(&mut self, name: &str) { + let Some(idx) = self.active_session_index else { + return; + }; + + // The DirectoryBrowser session cannot be renamed. + if self.sessions[idx].kind == CollectionKind::DirectoryBrowser { + return; + } + + if self.index_by_name(name).is_some() { + return; + } + + self.sessions[idx].name = name.to_string(); + } + + fn add_document(&mut self, path: &Path) { + self.state = DocumentState::Loading; + match load_document(path) { + Ok(content) => { + if let Some(session) = self.active_session_mut() { + if session.kind == CollectionKind::DocumentCollection + && !session.items.iter().any(|i| i.path == path) + { + session.items.push(SessionItem::new(path.to_path_buf(), 0)); + } + } + self.active_document_index = self + .active_session() + .and_then(|s| s.items.iter().position(|i| i.path == path)); + self.state = DocumentState::Loaded(content); + } + Err(e) => { + self.state = DocumentState::Error(e); + } + } + } + + fn select_document(&mut self, index: usize) { + if let Some(session) = self.active_session() { + if index < session.items.len() { + self.active_document_index = Some(index); + } + } + } + + fn copy_document(&mut self) { + let Some(idx) = self.active_document_index else { + return; + }; + self.clipboard = self + .active_session() + .and_then(|s| s.items.get(idx)) + .cloned(); + } + + fn paste_document(&mut self) { + let Some(item) = self.clipboard.clone() else { + return; + }; + + if !matches!( + self.active_session().map(|s| &s.kind), + Some(CollectionKind::DocumentCollection) + ) { + return; + } + + let new_index = { + let Some(session) = self.active_session_mut() else { + return; + }; + + if session + .items + .iter() + .any(|i| i.path == item.path && i.page_index == item.page_index) + { + return; + } + + session.items.push(item); + session.items.len() - 1 + }; + + self.active_document_index = Some(new_index); + } + + fn remove_document(&mut self) { + if !matches!( + self.active_session().map(|s| &s.kind), + Some(CollectionKind::DocumentCollection) + ) { + return; + } + + let Some(idx) = self.active_document_index else { + return; + }; + + let new_index = { + let Some(session) = self.active_session_mut() else { + return; + }; + + if idx >= session.items.len() { + return; + } + + session.items.remove(idx); + let new_len = session.items.len(); + if new_len == 0 { + None + } else { + Some(idx.min(new_len - 1)) + } + }; + + self.active_document_index = new_index; + } + + fn navigate_next(&mut self) { + let len = self.active_session().map_or(0, |s| s.items.len()); + if let Some(idx) = self.active_document_index { + if idx + 1 < len { + self.active_document_index = Some(idx + 1); + } + } + } + + fn navigate_previous(&mut self) { + if let Some(idx) = self.active_document_index { + if idx > 0 { + self.active_document_index = Some(idx - 1); + } + } + } + + fn move_document_up(&mut self) { + let Some(idx) = self.active_document_index else { + return; + }; + if idx == 0 { + return; + } + let Some(session) = self.active_session_mut() else { + return; + }; + session.items.swap(idx, idx - 1); + self.active_document_index = Some(idx - 1); + } + + fn move_document_down(&mut self) { + let Some(idx) = self.active_document_index else { + return; + }; + let Some(session) = self.active_session_mut() else { + return; + }; + if idx + 1 >= session.items.len() { + return; + } + session.items.swap(idx, idx + 1); + self.active_document_index = Some(idx + 1); + } + + fn move_document_to(&mut self, target: usize) { + let Some(idx) = self.active_document_index else { + return; + }; + let Some(session) = self.active_session_mut() else { + return; + }; + if idx == target || target >= session.items.len() { + return; + } + let item = session.items.remove(idx); + session.items.insert(target, item); + self.active_document_index = Some(target); + } +} diff --git a/src/document/mod.rs b/src/document/mod.rs new file mode 100644 index 0000000..f620821 --- /dev/null +++ b/src/document/mod.rs @@ -0,0 +1,24 @@ +// SPDX-License-Identifier: GPL-3.0-or-later +// src/document/mod.rs +// +// Document abstraction – the public surface of the document subsystem. + +pub mod command; +pub mod loader; +pub mod manager; +pub mod model; +pub mod session; +pub mod types; + +use self::model::{PortableModel, RasterModel, VectorModel}; + +/// The decoded content of a loaded document. +/// +/// This enum is internal to Noctua. The UI only ever sees a `RawImage` +/// produced by the render subsystem – it never matches on this type. +#[derive(Debug)] +pub enum DocumentContent { + Raster(RasterModel), + Vector(VectorModel), + Portable(PortableModel), +} diff --git a/src/document/model/mod.rs b/src/document/model/mod.rs new file mode 100644 index 0000000..7ede092 --- /dev/null +++ b/src/document/model/mod.rs @@ -0,0 +1,12 @@ +// SPDX-License-Identifier: GPL-3.0-or-later +// src/document/model/mod.rs +// +// The three concrete document models Noctua understands. + +pub mod portable; +pub mod raster; +pub mod vector; + +pub use portable::PortableModel; +pub use raster::RasterModel; +pub use vector::VectorModel; diff --git a/src/document/model/portable.rs b/src/document/model/portable.rs new file mode 100644 index 0000000..cdcdc43 --- /dev/null +++ b/src/document/model/portable.rs @@ -0,0 +1,14 @@ +// SPDX-License-Identifier: GPL-3.0-or-later +// src/document/model/portable.rs +// +// Portable document format model (PDF). + +use crate::document::types::PageLayout; +use std::path::PathBuf; + +#[derive(Clone, Debug)] +pub struct PortableModel { + pub path: PathBuf, + pub page_count: usize, + pub layout: PageLayout, +} diff --git a/src/document/model/raster.rs b/src/document/model/raster.rs new file mode 100644 index 0000000..83228bc --- /dev/null +++ b/src/document/model/raster.rs @@ -0,0 +1,12 @@ +// SPDX-License-Identifier: GPL-3.0-or-later +// src/document/model/raster.rs +// +// Raster image document model (JPEG, PNG, etc.). + +use std::path::PathBuf; + +#[derive(Clone, Debug)] +pub struct RasterModel { + pub path: PathBuf, + pub dimensions: (u32, u32), +} diff --git a/src/document/model/vector.rs b/src/document/model/vector.rs new file mode 100644 index 0000000..15e9bf9 --- /dev/null +++ b/src/document/model/vector.rs @@ -0,0 +1,13 @@ +// SPDX-License-Identifier: GPL-3.0-or-later +// src/document/model/vector.rs +// +// Vector image document model (SVG). + +use crate::document::types::ViewBox; +use std::path::PathBuf; + +#[derive(Clone, Debug)] +pub struct VectorModel { + pub path: PathBuf, + pub view_box: Option, +} diff --git a/src/document/session/command.rs b/src/document/session/command.rs new file mode 100644 index 0000000..4b74877 --- /dev/null +++ b/src/document/session/command.rs @@ -0,0 +1,66 @@ +// SPDX-License-Identifier: GPL-3.0-or-later +// src/document/session/command.rs +// +// Commands that operate on sessions and their document collections. + +use std::path::PathBuf; + +/// Commands operating on sessions and their document collections. +/// +/// Two implicit cursors exist in the manager: +/// - `active_session_index` – the currently active session +/// - `active_document_index` – the currently active document within that session +/// +/// `SelectSession` and `SelectDocument` set these cursors. All commands that +/// do not carry an explicit target operate on the active cursor. +/// +/// Only `DocumentCollection` sessions are user-managed. The single +/// `DirectoryBrowser` session is created by the UI and cannot be closed, +/// renamed, or removed. +#[derive(Debug, Clone)] +pub enum SessionCommand { + // Session lifecycle + /// Create a new empty `DocumentCollection` session. + NewSession { name: String }, + /// Load sessions from an existing .ron file. + OpenSession { path: PathBuf }, + /// Persist all sessions to the .ron file. + SaveSession, + /// Set the active session cursor by name. + SelectSession { name: String }, + /// Duplicate the active session as a new `DocumentCollection`. + /// + /// When the source is a `DirectoryBrowser` this produces a snapshot: + /// the current items are copied into a new `DocumentCollection`. + DuplicateSession { new_name: String }, + /// Delete the active `DocumentCollection` session permanently (removed from .ron on save). + DeleteSession, + /// Rename the active `DocumentCollection` session. + RenameSession { name: String }, + + // Collection management within the active session + /// Scan a directory and populate the active `DirectoryBrowser` session. + AddDirectoryToSession { dir: PathBuf }, + /// Add a single document to the active `DocumentCollection` session. + AddDocumentToSession { path: PathBuf }, + + // Document management within the active session + /// Set the active document cursor by index. + SelectDocument { index: usize }, + /// Copy the active document into the clipboard. + CopyDocument, + /// Paste the clipboard item at the end of the active session. + PasteDocument, + /// Remove the active document from the active session. + RemoveDocument, + /// Move the active document cursor to the next item. + NextDocument, + /// Move the active document cursor to the previous item. + PreviousDocument, + /// Move the active document one position towards the front. + MoveDocumentUp, + /// Move the active document one position towards the back. + MoveDocumentDown, + /// Move the active document to an explicit position. + MoveDocumentTo { index: usize }, +} diff --git a/src/document/session/data.rs b/src/document/session/data.rs new file mode 100644 index 0000000..9697fdd --- /dev/null +++ b/src/document/session/data.rs @@ -0,0 +1,72 @@ +// SPDX-License-Identifier: GPL-3.0-or-later +// src/document/session/data.rs +// +// Pure data structures for sessions and their document items. +// +// Operational state (active session cursor, active document cursor, clipboard) +// lives in the manager, not here. This module contains only what is persisted. + +use serde::{Deserialize, Serialize}; +use std::path::PathBuf; + +/// Determines how a session's items are managed. +/// +/// A `DirectoryBrowser` session is created once by the UI and cannot be closed +/// or removed by the user. It can be duplicated via `DuplicateSession`, which +/// produces a new `DocumentCollection` containing a snapshot of the current items. +#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)] +pub enum CollectionKind { + /// Items are derived from a directory scan; `path` holds the source directory. + DirectoryBrowser, + /// Items are a curated list of documents or pages; `path` is None. + DocumentCollection, +} + +/// A single entry in a session – a file path and an optional page within that file. +/// +/// `page_index` is 0 for single-page formats (raster, SVG) and non-zero for +/// multi-page documents (PDF, multi-frame TIFF) where each page is its own item. +#[derive(Debug, Clone, Serialize, Deserialize)] +pub struct SessionItem { + pub path: PathBuf, + pub page_index: usize, +} + +impl SessionItem { + #[must_use] + pub fn new(path: PathBuf, page_index: usize) -> Self { + Self { path, page_index } + } +} + +/// The persistable state of a single session. +/// +/// Operational state (active document index, clipboard) lives in the manager. +/// This struct is serialized as-is to the RON session file. +#[derive(Debug, Clone, Serialize, Deserialize)] +pub struct SessionData { + pub name: String, + pub kind: CollectionKind, + /// For `DirectoryBrowser`: the source directory to scan. + /// For `DocumentCollection`: `None` (items are managed individually). + #[serde(default)] + pub path: Option, + pub items: Vec, +} + +impl SessionData { + #[must_use] + pub fn new( + name: &str, + kind: CollectionKind, + path: Option, + items: Vec, + ) -> Self { + Self { + name: name.to_string(), + kind, + path, + items, + } + } +} diff --git a/src/document/session/mod.rs b/src/document/session/mod.rs new file mode 100644 index 0000000..5d25ca6 --- /dev/null +++ b/src/document/session/mod.rs @@ -0,0 +1,8 @@ +// SPDX-License-Identifier: GPL-3.0-or-later +// src/document/session/mod.rs +// +// Session subsystem: data model, commands, and persistence. + +pub mod command; +pub mod data; +pub mod store; diff --git a/src/document/session/store.rs b/src/document/session/store.rs new file mode 100644 index 0000000..9d00c3c --- /dev/null +++ b/src/document/session/store.rs @@ -0,0 +1,62 @@ +// SPDX-License-Identifier: GPL-3.0-or-later +// src/document/session/store.rs +// +// RON-based persistence for session data: load and save session lists. +// +// For the session concept and the meaning of each field see `session.rs`. +// +// On disk a session file contains a RON list of SessionData records: +// +// [ +// ( +// name: "Pictures", +// kind: DirectoryBrowser, +// path: Some("/home/user/Pictures"), +// items: [], +// current_index: None, +// ), +// ( +// name: "Project Docs", +// kind: DocumentCollection, +// path: None, +// items: [ +// (path: "/home/user/docs/spec.pdf", page_index: 0), +// (path: "/home/user/docs/notes.pdf", page_index: 2), +// ], +// current_index: Some(0), +// ), +// ] + +use crate::document::session::data::SessionData; +use crate::error::{Error, Result}; +use std::path::Path; + +/// Loads all sessions from a RON file. +/// +/// # Errors +/// +/// Returns [`Error::Io`] if the file cannot be read. +/// Returns [`Error::Session`] if the file cannot be parsed. +pub fn load_sessions(path: &Path) -> Result> { + let content = std::fs::read_to_string(path).map_err(Error::Io)?; + + ron::from_str(&content).map_err(|e| { + Error::Session(format!( + "Failed to parse session file {}: {e}", + path.display() + )) + }) +} + +/// Saves all sessions to a RON file. +/// +/// # Errors +/// +/// Returns [`Error::Session`] if the sessions cannot be serialized. +/// Returns [`Error::Io`] if the file cannot be written. +pub fn save_sessions(path: &Path, sessions: &[SessionData]) -> Result<()> { + let content = ron::ser::to_string_pretty(sessions, ron::ser::PrettyConfig::default()) + .map_err(|e| Error::Session(format!("Failed to serialize sessions: {e}")))?; + + std::fs::write(path, content).map_err(Error::Io) +} diff --git a/src/document/types/image.rs b/src/document/types/image.rs new file mode 100644 index 0000000..3791631 --- /dev/null +++ b/src/document/types/image.rs @@ -0,0 +1,17 @@ +// SPDX-License-Identifier: GPL-3.0-or-later +// src/document/types/image.rs +// +// Raw pixel buffer passed from the render subsystem to the UI. + +use std::sync::Arc; + +/// A decoded, format-independent pixel buffer ready for display. +/// +/// The UI receives this type from the `DocumentManager` and never needs to +/// know which document format (PDF, SVG, raster) produced it. +#[derive(Clone, Debug)] +pub struct RawImage { + pub data: Arc>, + pub width: u32, + pub height: u32, +} diff --git a/src/document/types/layout.rs b/src/document/types/layout.rs new file mode 100644 index 0000000..e86628d --- /dev/null +++ b/src/document/types/layout.rs @@ -0,0 +1,45 @@ +// SPDX-License-Identifier: GPL-3.0-or-later +// src/document/types/layout.rs +// +// PageLayout – describes the vertical arrangement of pages in a document. + +/// The computed layout of all pages in a multi-page document. +/// +/// Stores absolute y-offsets for each page so the render subsystem can +/// determine which pages are visible at a given scroll position without +/// iterating the full list on every frame. +#[derive(Clone, Debug)] +pub struct PageLayout { + pub page_starts: Vec, + pub page_sizes: Vec<(f64, f64)>, + pub gap: f64, + pub total_height: f64, + pub max_width: f64, +} + +impl PageLayout { + #[must_use] + pub fn new(page_sizes: Vec<(f64, f64)>, gap: f64) -> Self { + let mut page_starts = Vec::with_capacity(page_sizes.len()); + let mut y = 0.0_f64; + let mut max_width = 0.0_f64; + + for &(w, h) in &page_sizes { + page_starts.push(y); + y += h + gap; + if w > max_width { + max_width = w; + } + } + + let total_height = if page_sizes.is_empty() { 0.0 } else { y - gap }; + + Self { + page_starts, + page_sizes, + gap, + total_height, + max_width, + } + } +} diff --git a/src/document/types/mod.rs b/src/document/types/mod.rs new file mode 100644 index 0000000..7a5f836 --- /dev/null +++ b/src/document/types/mod.rs @@ -0,0 +1,11 @@ +// SPDX-License-Identifier: GPL-3.0-or-later +// src/document/types/mod.rs +// +// Shared domain primitives used across the document and render subsystems. + +pub mod image; +pub mod layout; +pub mod viewbox; + +pub use layout::PageLayout; +pub use viewbox::ViewBox; diff --git a/src/document/types/viewbox.rs b/src/document/types/viewbox.rs new file mode 100644 index 0000000..45715c6 --- /dev/null +++ b/src/document/types/viewbox.rs @@ -0,0 +1,16 @@ +// SPDX-License-Identifier: GPL-3.0-or-later +// src/document/types/viewbox.rs +// +// ViewBox – describes the visible coordinate space of a vector document. + +/// The coordinate space declared by an SVG `viewBox` attribute. +/// +/// Used by the render subsystem to map document coordinates to screen pixels +/// without exposing SVG internals to the UI. +#[derive(Clone, Debug)] +pub struct ViewBox { + pub x: f64, + pub y: f64, + pub width: f64, + pub height: f64, +} diff --git a/src/domain/document/collection.rs b/src/domain/document/collection.rs deleted file mode 100644 index 43c4c09..0000000 --- a/src/domain/document/collection.rs +++ /dev/null @@ -1,277 +0,0 @@ -// SPDX-License-Identifier: GPL-3.0-or-later -// src/domain/document/collection.rs -// -// Document collection for managing multiple documents. - -use std::path::PathBuf; - -use crate::domain::document::core::content::DocumentContent; - -/// A collection of documents with navigation support. -/// -/// This abstraction is useful for: -/// - Browsing through folders of images -/// - Batch operations on multiple documents -/// - Comparison views (showing multiple documents side-by-side) -#[derive(Debug)] -pub struct DocumentCollection { - /// List of document paths in the collection. - paths: Vec, - /// Currently active document index. - current_index: Option, - /// Currently loaded document (lazy-loaded). - current_document: Option, -} - -impl DocumentCollection { - /// Create an empty collection. - #[must_use] - pub fn new() -> Self { - Self { - paths: Vec::new(), - current_index: None, - current_document: None, - } - } - - /// Create a collection from a list of paths. - #[must_use] - pub fn from_paths(paths: Vec) -> Self { - let current_index = if paths.is_empty() { None } else { Some(0) }; - - Self { - paths, - current_index, - current_document: None, - } - } - - /// Get the number of documents in the collection. - #[must_use] - pub fn len(&self) -> usize { - self.paths.len() - } - - /// Check if the collection is empty. - #[must_use] - pub fn is_empty(&self) -> bool { - self.paths.is_empty() - } - - /// Get the current document index (0-based). - #[must_use] - pub fn current_index(&self) -> Option { - self.current_index - } - - /// Get the current document path. - #[must_use] - pub fn current_path(&self) -> Option<&PathBuf> { - self.current_index.and_then(|idx| self.paths.get(idx)) - } - - /// Get all paths in the collection. - #[must_use] - pub fn paths(&self) -> &[PathBuf] { - &self.paths - } - - /// Get a reference to the currently loaded document. - #[must_use] - pub fn current_document(&self) -> Option<&DocumentContent> { - self.current_document.as_ref() - } - - /// Get a mutable reference to the currently loaded document. - #[must_use] - pub fn current_document_mut(&mut self) -> Option<&mut DocumentContent> { - self.current_document.as_mut() - } - - /// Set the currently loaded document. - pub fn set_current_document(&mut self, document: DocumentContent) { - self.current_document = Some(document); - } - - /// Clear the currently loaded document. - pub fn clear_current_document(&mut self) { - self.current_document = None; - } - - /// Navigate to the next document in the collection. - /// - /// Returns the new index if successful, None if already at the end. - pub fn next(&mut self) -> Option { - if let Some(current) = self.current_index - && current + 1 < self.paths.len() - { - self.current_index = Some(current + 1); - self.current_document = None; // Clear document (needs reload) - return self.current_index; - } - None - } - - /// Navigate to the previous document in the collection. - /// - /// Returns the new index if successful, None if already at the start. - pub fn previous(&mut self) -> Option { - if let Some(current) = self.current_index - && current > 0 - { - self.current_index = Some(current - 1); - self.current_document = None; // Clear document (needs reload) - return self.current_index; - } - None - } - - /// Navigate to a specific index. - /// - /// Returns true if the index is valid and navigation succeeded. - pub fn goto(&mut self, index: usize) -> bool { - if index < self.paths.len() { - self.current_index = Some(index); - self.current_document = None; // Clear document (needs reload) - true - } else { - false - } - } - - /// Add a document path to the collection. - pub fn add_path(&mut self, path: PathBuf) { - self.paths.push(path); - if self.current_index.is_none() { - self.current_index = Some(0); - } - } - - /// Remove a document path at the given index. - /// - /// Returns the removed path if successful. - pub fn remove_at(&mut self, index: usize) -> Option { - if index < self.paths.len() { - let removed = self.paths.remove(index); - - // Update current index if needed - if let Some(current) = self.current_index { - if current == index { - // Removed current document - self.current_document = None; - if self.paths.is_empty() { - self.current_index = None; - } else if current >= self.paths.len() { - self.current_index = Some(self.paths.len() - 1); - } - } else if current > index { - // Adjust index after removal - self.current_index = Some(current - 1); - } - } - - Some(removed) - } else { - None - } - } - - /// Clear the entire collection. - pub fn clear(&mut self) { - self.paths.clear(); - self.current_index = None; - self.current_document = None; - } - - /// Check if there is a next document available. - #[must_use] - pub fn has_next(&self) -> bool { - if let Some(current) = self.current_index { - current + 1 < self.paths.len() - } else { - false - } - } - - /// Check if there is a previous document available. - #[must_use] - pub fn has_previous(&self) -> bool { - if let Some(current) = self.current_index { - current > 0 - } else { - false - } - } - - /// Get the path at a specific index. - #[must_use] - pub fn path_at(&self, index: usize) -> Option<&PathBuf> { - self.paths.get(index) - } -} - -impl Default for DocumentCollection { - fn default() -> Self { - Self::new() - } -} - -#[cfg(test)] -mod tests { - use super::*; - - #[test] - fn test_empty_collection() { - let collection = DocumentCollection::new(); - assert!(collection.is_empty()); - assert_eq!(collection.len(), 0); - assert_eq!(collection.current_index(), None); - } - - #[test] - fn test_navigation() { - let paths = vec![ - PathBuf::from("a.png"), - PathBuf::from("b.png"), - PathBuf::from("c.png"), - ]; - let mut collection = DocumentCollection::from_paths(paths); - - assert_eq!(collection.current_index(), Some(0)); - assert_eq!(collection.next(), Some(1)); - assert_eq!(collection.next(), Some(2)); - assert_eq!(collection.next(), None); // At end - assert_eq!(collection.previous(), Some(1)); - assert_eq!(collection.previous(), Some(0)); - assert_eq!(collection.previous(), None); // At start - } - - #[test] - fn test_goto() { - let paths = vec![ - PathBuf::from("a.png"), - PathBuf::from("b.png"), - PathBuf::from("c.png"), - ]; - let mut collection = DocumentCollection::from_paths(paths); - - assert!(collection.goto(2)); - assert_eq!(collection.current_index(), Some(2)); - assert!(!collection.goto(10)); // Invalid index - } - - #[test] - fn test_remove() { - let paths = vec![ - PathBuf::from("a.png"), - PathBuf::from("b.png"), - PathBuf::from("c.png"), - ]; - let mut collection = DocumentCollection::from_paths(paths); - - collection.goto(1); - assert_eq!(collection.remove_at(1), Some(PathBuf::from("b.png"))); - assert_eq!(collection.len(), 2); - assert_eq!(collection.current_index(), Some(1)); // Now points to c.png - } -} diff --git a/src/domain/document/core/content.rs b/src/domain/document/core/content.rs deleted file mode 100644 index 3cdf26c..0000000 --- a/src/domain/document/core/content.rs +++ /dev/null @@ -1,403 +0,0 @@ -// SPDX-License-Identifier: GPL-3.0-or-later -// src/domain/document/core/content.rs -// -// Type-erased document content enum. - -use std::fmt; -use std::path::Path; - -use cosmic::iced_renderer::graphics::image::image_rs::ImageFormat as CosmicImageFormat; -use cosmic::widget::image::Handle as ImageHandle; - -use super::document::{ - DocResult, DocumentInfo, FlipDirection, InterpolationQuality, MultiPage, MultiPageThumbnails, - RenderOutput, Renderable, Rotation, RotationMode, Transformable, TransformState, -}; - -use crate::domain::document::types::raster::RasterDocument; -#[cfg(feature = "vector")] -use crate::domain::document::types::vector::VectorDocument; -#[cfg(feature = "portable")] -use crate::domain::document::types::portable::PortableDocument; - -// ============================================================================ -// Document Kind -// ============================================================================ - -/// Supported document kinds (for format detection). -#[derive(Debug, Clone, Copy, PartialEq, Eq)] -pub enum DocumentKind { - Raster, - Vector, - Portable, -} - -impl DocumentKind { - /// Detect document kind from file path. - #[must_use] - pub fn from_path(path: &Path) -> Option { - let ext = path.extension()?.to_str()?.to_lowercase(); - - // SVG - #[cfg(feature = "vector")] - if ext == "svg" || ext == "svgz" { - return Some(Self::Vector); - } - - // PDF - #[cfg(feature = "portable")] - if ext == "pdf" { - return Some(Self::Portable); - } - - // Raster: Check via cosmic/image-rs - if CosmicImageFormat::from_path(path).is_ok() { - return Some(Self::Raster); - } - - None - } -} - -impl fmt::Display for DocumentKind { - fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { - match self { - Self::Raster => write!(f, "Raster"), - Self::Vector => write!(f, "Vector"), - Self::Portable => write!(f, "Portable"), - } - } -} - -// ============================================================================ -// Document Content Enum -// ============================================================================ - -/// Type-erased document content. -/// -/// The application only holds one document at a time, so the size difference -/// between variants is acceptable. Boxing would add unnecessary indirection. -#[allow(clippy::large_enum_variant)] -pub enum DocumentContent { - Raster(RasterDocument), - #[cfg(feature = "vector")] - Vector(VectorDocument), - #[cfg(feature = "portable")] - Portable(PortableDocument), -} - -impl fmt::Debug for DocumentContent { - fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { - match self { - Self::Raster(_) => write!(f, "DocumentContent::Raster(...)"), - #[cfg(feature = "vector")] - Self::Vector(_) => write!(f, "DocumentContent::Vector(...)"), - #[cfg(feature = "portable")] - Self::Portable(_) => write!(f, "DocumentContent::Portable(...)"), - } - } -} - -// ============================================================================ -// Trait Implementations -// ============================================================================ - -impl Renderable for DocumentContent { - fn render(&mut self, scale: f64) -> DocResult { - match self { - Self::Raster(doc) => doc.render(scale), - #[cfg(feature = "vector")] - Self::Vector(doc) => doc.render(scale), - #[cfg(feature = "portable")] - Self::Portable(doc) => doc.render(scale), - } - } - - fn info(&self) -> DocumentInfo { - match self { - Self::Raster(doc) => doc.info(), - #[cfg(feature = "vector")] - Self::Vector(doc) => doc.info(), - #[cfg(feature = "portable")] - Self::Portable(doc) => doc.info(), - } - } -} - -impl Transformable for DocumentContent { - fn rotate(&mut self, rotation: Rotation) { - match self { - Self::Raster(doc) => doc.rotate(rotation), - #[cfg(feature = "vector")] - Self::Vector(doc) => doc.rotate(rotation), - #[cfg(feature = "portable")] - Self::Portable(doc) => doc.rotate(rotation), - } - } - - fn flip(&mut self, direction: FlipDirection) { - match self { - Self::Raster(doc) => doc.flip(direction), - #[cfg(feature = "vector")] - Self::Vector(doc) => doc.flip(direction), - #[cfg(feature = "portable")] - Self::Portable(doc) => doc.flip(direction), - } - } - - fn transform_state(&self) -> TransformState { - match self { - Self::Raster(doc) => doc.transform_state(), - #[cfg(feature = "vector")] - Self::Vector(doc) => doc.transform_state(), - #[cfg(feature = "portable")] - Self::Portable(doc) => doc.transform_state(), - } - } - - fn rotate_fine(&mut self, angle_degrees: f32) { - match self { - Self::Raster(doc) => doc.rotate_fine(angle_degrees), - #[cfg(feature = "vector")] - Self::Vector(doc) => doc.rotate_fine(angle_degrees), - #[cfg(feature = "portable")] - Self::Portable(doc) => doc.rotate_fine(angle_degrees), - } - } - - fn reset_fine_rotation(&mut self) { - match self { - Self::Raster(doc) => doc.reset_fine_rotation(), - #[cfg(feature = "vector")] - Self::Vector(doc) => doc.reset_fine_rotation(), - #[cfg(feature = "portable")] - Self::Portable(doc) => doc.reset_fine_rotation(), - } - } - - fn set_interpolation_quality(&mut self, quality: InterpolationQuality) { - match self { - Self::Raster(doc) => doc.set_interpolation_quality(quality), - #[cfg(feature = "vector")] - Self::Vector(doc) => doc.set_interpolation_quality(quality), - #[cfg(feature = "portable")] - Self::Portable(doc) => doc.set_interpolation_quality(quality), - } - } -} - -// ============================================================================ -// Convenience Methods -// ============================================================================ - -impl DocumentContent { - /// Rotate document 90 degrees clockwise. - pub fn rotate_cw(&mut self) { - let new_rotation_mode = self.transform_state().rotation.rotate_cw(); - match new_rotation_mode { - RotationMode::Standard(rot) => self.rotate(rot), - RotationMode::Fine(deg) => { - let normalized = ((deg / 90.0).round() as i16 * 90) % 360; - let rot = match normalized { - 0 => Rotation::None, - 90 => Rotation::Cw90, - 180 => Rotation::Cw180, - 270 => Rotation::Cw270, - _ => Rotation::None, - }; - self.rotate(rot); - } - } - } - - /// Rotate document 90 degrees counter-clockwise. - pub fn rotate_ccw(&mut self) { - let new_rotation_mode = self.transform_state().rotation.rotate_ccw(); - match new_rotation_mode { - RotationMode::Standard(rot) => self.rotate(rot), - RotationMode::Fine(deg) => { - let normalized = ((deg / 90.0).round() as i16 * 90 + 360) % 360; - let rot = match normalized { - 0 => Rotation::None, - 90 => Rotation::Cw90, - 180 => Rotation::Cw180, - 270 => Rotation::Cw270, - _ => Rotation::None, - }; - self.rotate(rot); - } - } - } - - /// Flip document horizontally. - pub fn flip_horizontal(&mut self) { - self.flip(FlipDirection::Horizontal); - } - - /// Flip document vertically. - pub fn flip_vertical(&mut self) { - self.flip(FlipDirection::Vertical); - } - - /// Get the document kind. - #[must_use] - pub fn kind(&self) -> DocumentKind { - match self { - Self::Raster(_) => DocumentKind::Raster, - #[cfg(feature = "vector")] - Self::Vector(_) => DocumentKind::Vector, - #[cfg(feature = "portable")] - Self::Portable(_) => DocumentKind::Portable, - } - } - - /// Check if document supports multiple pages. - #[must_use] - pub fn is_multi_page(&self) -> bool { - matches!(self, Self::Portable(_)) - } - - /// Get total page count (returns 1 for single-page documents). - #[must_use] - pub fn page_count(&self) -> usize { - match self { - #[cfg(feature = "portable")] - Self::Portable(doc) => doc.page_count(), - _ => 1, - } - } - - /// Get current page index (0 for single-page documents). - #[must_use] - pub fn current_page(&self) -> usize { - match self { - #[cfg(feature = "portable")] - Self::Portable(doc) => doc.current_page(), - _ => 0, - } - } - - /// Navigate to a specific page (no-op for single-page documents). - pub fn go_to_page(&mut self, page: usize) -> DocResult<()> { - match self { - #[cfg(feature = "portable")] - Self::Portable(doc) => doc.go_to_page(page), - _ => Ok(()), - } - } - - /// Get thumbnail for a specific page (mutable access for trait compatibility). - pub fn get_thumbnail(&mut self, page: usize) -> DocResult> { - match self { - #[cfg(feature = "portable")] - Self::Portable(doc) => doc.get_thumbnail(page), - _ => Ok(None), - } - } - - /// Get thumbnail handle for a specific page (read-only access). - /// Returns None if the thumbnail hasn't been generated yet. - #[must_use] - pub fn get_thumbnail_handle(&self, page: usize) -> Option { - match self { - #[cfg(feature = "portable")] - Self::Portable(doc) => doc.get_thumbnail_handle(page), - _ => None, - } - } - - /// Check if thumbnails are ready to be generated. - #[must_use] - pub fn thumbnails_ready(&self) -> bool { - match self { - #[cfg(feature = "portable")] - Self::Portable(doc) => doc.thumbnails_ready(), - _ => false, - } - } - - /// Get count of thumbnails currently loaded. - #[must_use] - pub fn thumbnails_loaded(&self) -> usize { - match self { - #[cfg(feature = "portable")] - Self::Portable(doc) => PortableDocument::thumbnails_loaded(doc), - _ => 0, - } - } - - /// Check if all thumbnails have been loaded (trait-compliant). - #[must_use] - pub fn all_thumbnails_loaded(&self) -> bool { - match self { - #[cfg(feature = "portable")] - Self::Portable(doc) => MultiPageThumbnails::thumbnails_loaded(doc), - _ => false, - } - } - - /// Generate thumbnail for a specific page. - pub fn generate_thumbnail_page(&mut self, page: usize) -> DocResult<()> { - match self { - #[cfg(feature = "portable")] - Self::Portable(doc) => MultiPageThumbnails::generate_thumbnail_page(doc, page), - _ => Ok(()), - } - } - - /// Generate all thumbnails. - pub fn generate_thumbnails(&mut self) -> DocResult<()> { - match self { - #[cfg(feature = "portable")] - Self::Portable(doc) => MultiPageThumbnails::generate_all_thumbnails(doc), - _ => Ok(()), - } - } - - /// Get the current rendered image handle. - #[must_use] - pub fn handle(&self) -> Option { - match self { - Self::Raster(doc) => Some(doc.handle()), - #[cfg(feature = "vector")] - Self::Vector(doc) => Some(doc.handle()), - #[cfg(feature = "portable")] - Self::Portable(doc) => Some(doc.handle()), - } - } - - /// Get current dimensions after transformations. - #[must_use] - pub fn dimensions(&self) -> (u32, u32) { - match self { - Self::Raster(doc) => doc.dimensions(), - #[cfg(feature = "vector")] - Self::Vector(doc) => doc.dimensions(), - #[cfg(feature = "portable")] - Self::Portable(doc) => doc.dimensions(), - } - } - - /// Crop the document (supported for all types - works on rendered output). - pub fn crop(&mut self, x: u32, y: u32, width: u32, height: u32) -> DocResult<()> { - match self { - Self::Raster(doc) => doc.crop(x, y, width, height).map_err(|e| anyhow::anyhow!(e)), - #[cfg(feature = "vector")] - Self::Vector(doc) => doc.crop(x, y, width, height).map_err(|e| anyhow::anyhow!(e)), - #[cfg(feature = "portable")] - Self::Portable(doc) => doc.crop(x, y, width, height).map_err(|e| anyhow::anyhow!(e)), - } - } - - /// Extract document metadata (basic info and EXIF if available). - #[must_use] - pub fn extract_meta(&self, path: &Path) -> crate::domain::document::core::metadata::DocumentMeta { - match self { - Self::Raster(doc) => doc.extract_meta(path), - #[cfg(feature = "vector")] - Self::Vector(doc) => doc.extract_meta(path), - #[cfg(feature = "portable")] - Self::Portable(doc) => doc.extract_meta(path), - } - } -} diff --git a/src/domain/document/core/document.rs b/src/domain/document/core/document.rs deleted file mode 100644 index 3e1fdd7..0000000 --- a/src/domain/document/core/document.rs +++ /dev/null @@ -1,250 +0,0 @@ -// SPDX-License-Identifier: GPL-3.0-or-later -// src/domain/document/core/document.rs -// -// Core document traits and abstractions. - -use cosmic::widget::image::Handle as ImageHandle; - -// ============================================================================ -// Type Definitions -// ============================================================================ - -/// Result type alias for document operations. -pub type DocResult = anyhow::Result; - -/// Rotation state for documents. -#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)] -pub enum Rotation { - /// No rotation (0 degrees). - #[default] - None, - /// 90 degrees clockwise. - Cw90, - /// 180 degrees. - Cw180, - /// 270 degrees clockwise (90 counter-clockwise). - Cw270, -} - -impl Rotation { - /// Rotate clockwise by 90 degrees. - #[must_use] - pub fn rotate_cw(self) -> Self { - match self { - Self::None => Self::Cw90, - Self::Cw90 => Self::Cw180, - Self::Cw180 => Self::Cw270, - Self::Cw270 => Self::None, - } - } - - /// Rotate counter-clockwise by 90 degrees. - #[must_use] - pub fn rotate_ccw(self) -> Self { - match self { - Self::None => Self::Cw270, - Self::Cw270 => Self::Cw180, - Self::Cw180 => Self::Cw90, - Self::Cw90 => Self::None, - } - } - - /// Convert to degrees (0, 90, 180, 270). - #[must_use] - pub fn to_degrees(self) -> i16 { - match self { - Self::None => 0, - Self::Cw90 => 90, - Self::Cw180 => 180, - Self::Cw270 => 270, - } - } -} - -/// Rotation mode: standard 90° steps or fine-grained rotation. -#[derive(Debug, Clone, Copy, PartialEq)] -pub enum RotationMode { - /// Standard 90° rotation (lossless for most formats). - Standard(Rotation), - /// Fine-grained rotation in degrees (0.0 - 360.0) with interpolation. - Fine(f32), -} - -impl Default for RotationMode { - fn default() -> Self { - Self::Standard(Rotation::None) - } -} - -impl RotationMode { - /// Convert rotation to degrees (0.0 - 360.0). - #[must_use] - pub fn to_degrees(self) -> f32 { - match self { - Self::Standard(r) => f32::from(r.to_degrees()), - Self::Fine(deg) => deg, - } - } - - /// Check if rotation is a multiple of 90 degrees. - #[must_use] - pub fn is_multiple_of_90(self) -> bool { - match self { - Self::Standard(_) => true, - Self::Fine(deg) => (deg % 90.0).abs() < 0.01, - } - } - - /// Check if no rotation is applied. - #[must_use] - pub fn is_none(self) -> bool { - match self { - Self::Standard(Rotation::None) => true, - Self::Standard(_) => false, - Self::Fine(deg) => deg.abs() < 0.01, - } - } - - /// Rotate clockwise by 90 degrees. - #[must_use] - pub fn rotate_cw(self) -> Self { - match self { - Self::Standard(r) => Self::Standard(r.rotate_cw()), - Self::Fine(deg) => Self::Fine((deg + 90.0) % 360.0), - } - } - - /// Rotate counter-clockwise by 90 degrees. - #[must_use] - pub fn rotate_ccw(self) -> Self { - match self { - Self::Standard(r) => Self::Standard(r.rotate_ccw()), - Self::Fine(deg) => Self::Fine((deg - 90.0 + 360.0) % 360.0), - } - } -} - -/// Interpolation quality for fine rotation and resizing operations. -#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)] -#[allow(dead_code)] -pub enum InterpolationQuality { - /// Fast, nearest neighbor interpolation. - Fast, - /// Balanced bilinear interpolation (default). - #[default] - Balanced, - /// Best quality, bicubic interpolation. - Best, -} - -/// Flip direction for documents. -#[derive(Debug, Clone, Copy, PartialEq, Eq)] -pub enum FlipDirection { - /// Flip along the vertical axis (mirror left-right). - Horizontal, - /// Flip along the horizontal axis (mirror top-bottom). - Vertical, -} - -/// Current transformation state of a document. -#[derive(Debug, Clone, Copy, Default, PartialEq)] -pub struct TransformState { - /// Current rotation mode (standard 90° or fine rotation). - pub rotation: RotationMode, - /// Whether flipped horizontally. - pub flip_h: bool, - /// Whether flipped vertically. - pub flip_v: bool, -} - -/// Output of a render operation. -#[derive(Debug, Clone)] -pub struct RenderOutput { - /// Image handle for display. - pub handle: ImageHandle, - /// Rendered width in pixels. - pub width: u32, - /// Rendered height in pixels. - pub height: u32, -} - -/// Document metadata/information. -#[derive(Debug, Clone)] -pub struct DocumentInfo { - /// Native width in pixels (before transforms). - pub width: u32, - /// Native height in pixels (before transforms). - pub height: u32, - /// Document format description. - pub format: String, -} - -// ============================================================================ -// Traits -// ============================================================================ - -/// Trait for documents that can be rendered to an image. -pub trait Renderable { - /// Render the document at the given scale factor. - fn render(&mut self, scale: f64) -> DocResult; - - /// Get document information (dimensions, format). - fn info(&self) -> DocumentInfo; -} - -/// Trait for documents that support geometric transformations. -pub trait Transformable { - /// Apply a standard 90° rotation. - fn rotate(&mut self, rotation: Rotation); - - /// Flip in the given direction. - fn flip(&mut self, direction: FlipDirection); - - /// Get the current transformation state. - fn transform_state(&self) -> TransformState; - - /// Apply fine-grained rotation in degrees (0.0 - 360.0). - fn rotate_fine(&mut self, _angle_degrees: f32) { - // Default: no-op (not all formats support fine rotation) - } - - /// Reset any accumulated fine rotation. - fn reset_fine_rotation(&mut self) { - // Default: no-op - } - - /// Set interpolation quality for transformations. - fn set_interpolation_quality(&mut self, _quality: InterpolationQuality) { - // Default: no-op - } -} - -/// Trait for documents with multiple pages. -pub trait MultiPage { - /// Get total number of pages. - fn page_count(&self) -> usize; - - /// Get current page index (0-based). - fn current_page(&self) -> usize; - - /// Navigate to a specific page. - fn go_to_page(&mut self, page: usize) -> DocResult<()>; -} - -/// Trait for multi-page documents that support thumbnail generation. -pub trait MultiPageThumbnails: MultiPage { - /// Get thumbnail for a specific page. - fn get_thumbnail(&mut self, page: usize) -> DocResult>; - - /// Check if thumbnails are ready to be generated. - fn thumbnails_ready(&self) -> bool; - - /// Check if all thumbnails have been loaded. - fn thumbnails_loaded(&self) -> bool; - - /// Generate thumbnail for a specific page. - fn generate_thumbnail_page(&mut self, page: usize) -> DocResult<()>; - - /// Generate all thumbnails. - fn generate_all_thumbnails(&mut self) -> DocResult<()>; -} diff --git a/src/domain/document/core/metadata.rs b/src/domain/document/core/metadata.rs deleted file mode 100644 index b62db25..0000000 --- a/src/domain/document/core/metadata.rs +++ /dev/null @@ -1,197 +0,0 @@ -// SPDX-License-Identifier: GPL-3.0-or-later -// src/domain/document/core/metadata.rs -// -// Document metadata structures and EXIF parsing. - -use std::io::Cursor; - -/// Minutes per degree for GPS coordinate conversion (DMS to decimal degrees). -const MINUTES_PER_DEGREE: f64 = 60.0; - -/// Seconds per degree for GPS coordinate conversion (DMS to decimal degrees). -const SECONDS_PER_DEGREE: f64 = 3600.0; - -/// Basic document metadata (always available). -#[derive(Debug, Clone)] -pub struct BasicMeta { - /// File name (without path). - pub file_name: String, - /// Full file path. - pub file_path: String, - /// Image format as string (e.g., "PNG", "JPEG", "PDF"). - pub format: String, - /// Width in pixels. - pub width: u32, - /// Height in pixels. - pub height: u32, - /// File size in bytes. - pub file_size: u64, - /// Color type description (e.g., "RGBA8", "RGB8", "Grayscale"). - pub color_type: String, -} - -impl BasicMeta { - /// Format file size as human-readable string. - pub fn file_size_display(&self) -> String { - const KB: u64 = 1024; - const MB: u64 = KB * 1024; - const GB: u64 = MB * 1024; - - #[allow(clippy::cast_precision_loss)] - if self.file_size >= GB { - let size_gb = self.file_size as f64 / GB as f64; - format!("{size_gb:.2} GB") - } else if self.file_size >= MB { - let size_mb = self.file_size as f64 / MB as f64; - format!("{size_mb:.2} MB") - } else if self.file_size >= KB { - let size_kb = self.file_size as f64 / KB as f64; - format!("{size_kb:.1} KB") - } else { - let size = self.file_size; - format!("{size} B") - } - } - - /// Format resolution as "W × H". - pub fn resolution_display(&self) -> String { - format!("{} × {}", self.width, self.height) - } -} - -/// EXIF metadata (optional, mainly for JPEG/TIFF). -#[derive(Debug, Clone, Default)] -pub struct ExifMeta { - pub camera_make: Option, - pub camera_model: Option, - pub date_time: Option, - pub exposure_time: Option, - pub f_number: Option, - pub iso: Option, - pub focal_length: Option, - pub gps_latitude: Option, - pub gps_longitude: Option, -} - -impl ExifMeta { - /// Parse EXIF data from raw image bytes. - /// - /// Extracts camera information, exposure settings, and GPS coordinates - /// from JPEG/TIFF EXIF metadata using the kamadak-exif crate. - pub fn from_bytes(bytes: &[u8]) -> Option { - use exif::{In, Reader, Tag}; - - let cursor = Cursor::new(bytes); - let exif_reader = Reader::new(); - let exif = exif_reader.read_from_container(&mut cursor.clone()).ok()?; - - let mut meta = Self::default(); - - // Camera make and model - if let Some(field) = exif.get_field(Tag::Make, In::PRIMARY) { - meta.camera_make = Some(field.display_value().to_string().trim().to_string()); - } - if let Some(field) = exif.get_field(Tag::Model, In::PRIMARY) { - meta.camera_model = Some(field.display_value().to_string().trim().to_string()); - } - - // Date and time - if let Some(field) = exif.get_field(Tag::DateTime, In::PRIMARY) { - meta.date_time = Some(field.display_value().to_string()); - } - - // Exposure time - if let Some(field) = exif.get_field(Tag::ExposureTime, In::PRIMARY) { - meta.exposure_time = Some(field.display_value().to_string()); - } - - // F-number (aperture) - if let Some(field) = exif.get_field(Tag::FNumber, In::PRIMARY) { - meta.f_number = Some(field.display_value().to_string()); - } - - // ISO speed - if let Some(field) = exif.get_field(Tag::PhotographicSensitivity, In::PRIMARY) { - if let exif::Value::Short(ref vec) = field.value { - if let Some(&iso) = vec.first() { - meta.iso = Some(u32::from(iso)); - } - } - } - - // Focal length - if let Some(field) = exif.get_field(Tag::FocalLength, In::PRIMARY) { - meta.focal_length = Some(field.display_value().to_string()); - } - - // GPS coordinates - meta.gps_latitude = Self::parse_gps_coord(&exif, Tag::GPSLatitude, Tag::GPSLatitudeRef); - meta.gps_longitude = Self::parse_gps_coord(&exif, Tag::GPSLongitude, Tag::GPSLongitudeRef); - - Some(meta) - } - - /// Parse GPS coordinate from EXIF data (converts DMS to decimal degrees). - fn parse_gps_coord(exif: &exif::Exif, coord_tag: exif::Tag, ref_tag: exif::Tag) -> Option { - use exif::{In, Value}; - - let coord_field = exif.get_field(coord_tag, In::PRIMARY)?; - let ref_field = exif.get_field(ref_tag, In::PRIMARY)?; - - // Get reference (N/S for latitude, E/W for longitude) - let reference = ref_field.display_value().to_string(); - - // Parse DMS (Degrees, Minutes, Seconds) values - if let Value::Rational(ref rationals) = coord_field.value { - if rationals.len() >= 3 { - let degrees = rationals[0].to_f64(); - let minutes = rationals[1].to_f64(); - let seconds = rationals[2].to_f64(); - - // Convert to decimal degrees - let mut decimal = - degrees + (minutes / MINUTES_PER_DEGREE) + (seconds / SECONDS_PER_DEGREE); - - // Apply sign based on hemisphere - if reference == "S" || reference == "W" { - decimal = -decimal; - } - - return Some(decimal); - } - } - - None - } - - /// Combined camera make and model for display. - pub fn camera_display(&self) -> Option { - match (&self.camera_make, &self.camera_model) { - (Some(make), Some(model)) => { - if model.starts_with(make) { - Some(model.clone()) - } else { - Some(format!("{make} {model}")) - } - } - (Some(make), None) => Some(make.clone()), - (None, Some(model)) => Some(model.clone()), - (None, None) => None, - } - } - - /// Format GPS coordinates for display. - pub fn gps_display(&self) -> Option { - match (self.gps_latitude, self.gps_longitude) { - (Some(lat), Some(lon)) => Some(format!("{lat:.5}, {lon:.5}")), - _ => None, - } - } -} - -/// Complete document metadata container. -#[derive(Debug, Clone)] -pub struct DocumentMeta { - pub basic: BasicMeta, - pub exif: Option, -} diff --git a/src/domain/document/core/mod.rs b/src/domain/document/core/mod.rs deleted file mode 100644 index cb37441..0000000 --- a/src/domain/document/core/mod.rs +++ /dev/null @@ -1,13 +0,0 @@ -// SPDX-License-Identifier: GPL-3.0-or-later -// src/domain/document/core/mod.rs -// -// Core document abstractions: traits, types, and metadata. - -pub mod content; -pub mod document; -pub mod metadata; -pub mod page; - -// Re-export commonly used types -pub use content::DocumentContent; -pub use metadata::DocumentMeta; diff --git a/src/domain/document/core/page.rs b/src/domain/document/core/page.rs deleted file mode 100644 index 98929e6..0000000 --- a/src/domain/document/core/page.rs +++ /dev/null @@ -1,74 +0,0 @@ -// SPDX-License-Identifier: GPL-3.0-or-later -// src/domain/document/core/page.rs -// -// Page abstraction for multi-page documents. - -use cosmic::widget::image::Handle as ImageHandle; - -/// Represents a single page in a multi-page document. -#[derive(Debug, Clone)] -#[allow(dead_code)] -pub struct Page { - /// Page index (0-based). - pub index: usize, - /// Page width in pixels. - pub width: u32, - /// Page height in pixels. - pub height: u32, - /// Optional thumbnail handle. - pub thumbnail: Option, -} - -impl Page { - /// Create a new page. - #[must_use] - pub fn new(index: usize, width: u32, height: u32) -> Self { - Self { - index, - width, - height, - thumbnail: None, - } - } - - /// Create a page with a thumbnail. - #[must_use] - pub fn with_thumbnail(index: usize, width: u32, height: u32, thumbnail: ImageHandle) -> Self { - Self { - index, - width, - height, - thumbnail: Some(thumbnail), - } - } - - /// Set the thumbnail for this page. - pub fn set_thumbnail(&mut self, thumbnail: ImageHandle) { - self.thumbnail = Some(thumbnail); - } - - /// Check if this page has a thumbnail. - #[must_use] - pub fn has_thumbnail(&self) -> bool { - self.thumbnail.is_some() - } - - /// Get the aspect ratio of the page. - #[must_use] - pub fn aspect_ratio(&self) -> f32 { - if self.height == 0 { - 1.0 - } else { - #[allow(clippy::cast_precision_loss)] - { - self.width as f32 / self.height as f32 - } - } - } - - /// Get page dimensions as a tuple. - #[must_use] - pub fn dimensions(&self) -> (u32, u32) { - (self.width, self.height) - } -} diff --git a/src/domain/document/mod.rs b/src/domain/document/mod.rs deleted file mode 100644 index d4211e9..0000000 --- a/src/domain/document/mod.rs +++ /dev/null @@ -1,17 +0,0 @@ -// SPDX-License-Identifier: GPL-3.0-or-later -// src/domain/document/mod.rs -// -// Document domain: core abstractions, types, and operations. - -pub mod collection; -pub mod core; -pub mod operations; -pub mod types; - -// Re-export core abstractions (only used ones) -#[allow(unused_imports)] -pub use core::{DocumentContent, DocumentMeta}; - -// Note: Low-level pixel operations (apply_rotation, apply_flip, crop_image) -// are internal helpers used only by document type implementations. -// Use high-level operations above for all application and UI code. diff --git a/src/domain/document/operations/README.md b/src/domain/document/operations/README.md deleted file mode 100644 index 2dfb964..0000000 --- a/src/domain/document/operations/README.md +++ /dev/null @@ -1,281 +0,0 @@ -# Document Operations - -This module provides transformation, rendering, and export operations for documents. - -## Architecture: Two-Level Operations - -The operations module is designed with **two distinct levels** of abstraction: - -### 1. Low-Level Operations (Internal/Private) - -**Purpose:** Direct manipulation of pixel data for raster images. - -**Visibility:** `pub(crate)` - Internal to the crate only. - -**Location:** `transform.rs` (internal helpers) - -**Functions:** -- `apply_rotation(img, rotation)` - Rotate raster pixels -- `apply_flip(img, direction)` - Flip raster pixels -- `crop_to_image(img, x, y, w, h)` - Crop raster to image - -**When to use:** -- ONLY in document type implementations (RasterDocument, VectorDocument, PortableDocument) -- NOT accessible outside the crate -- NOT for application or UI code - -**Example:** -```rust -// INTERNAL USE ONLY - in document type implementations -impl Transformable for RasterDocument { - fn rotate(&mut self, rotation: Rotation) { - // Low-level operation used internally - self.image = apply_rotation(self.image, rotation); - } -} -``` - -### 2. High-Level Operations (Type-Agnostic) - -**Purpose:** Document transformations that work across **all** document types (Raster, Vector, Portable). - -**Location:** `transform.rs` (high-level section) - -**Functions:** -- `rotate_document_cw(document)` - Rotate any document 90° CW -- `rotate_document_ccw(document)` - Rotate any document 90° CCW -- `flip_document_horizontal(document)` - Flip any document horizontally -- `flip_document_vertical(document)` - Flip any document vertically -- `rotate_document_to(document, rotation)` - Rotate to specific angle -- `reset_document_transforms(document)` - Reset all transformations - -**When to use:** -- In application commands (`TransformDocumentCommand`) -- In UI message handlers -- Anywhere you work with `DocumentContent` (type-erased document) - -**Example:** -```rust -use crate::domain::document::operations::transform; - -// RECOMMENDED: Use high-level operations -let mut document = DocumentContent::Raster(raster_doc); -transform::rotate_document_cw(&mut document)?; -transform::flip_document_horizontal(&mut document)?; - -// Works with Vector and Portable too! -let mut svg = DocumentContent::Vector(vector_doc); -transform::rotate_document_cw(&mut svg)?; // Lossless viewport transform - -// Works with PDF! -let mut pdf = DocumentContent::Portable(portable_doc); -transform::rotate_document_cw(&mut pdf)?; // Backend handles rendering -``` - -## Why This Separation? - -### Why Low-Level Operations Are Internal - -**Problem:** Exposing low-level operations creates confusion: -- Developers don't know whether to use `apply_rotation()` or `rotate_document_cw()` -- Low-level operations only work on `DynamicImage`, not `DocumentContent` -- Creates two ways to do the same thing (violates DRY) - -**Solution:** Make them `pub(crate)`: -```rust -// NOT POSSIBLE - apply_rotation is internal -transform::apply_rotation(img, Rotation::Cw90); // Compile error! - -// USE THIS - high-level operation -transform::rotate_document_cw(&mut document)?; // Works! -``` - -### Why High-Level Operations Exist - -**Problem without them:** -```rust -// Coupled to implementation details -match document { - DocumentContent::Raster(ref mut doc) => doc.rotate(Rotation::Cw90), - DocumentContent::Vector(ref mut doc) => doc.rotate(Rotation::Cw90), - DocumentContent::Portable(ref mut doc) => doc.rotate(Rotation::Cw90), -} -``` - -**Solution:** -```rust -// Single API for all types -transform::rotate_document_cw(&mut document)?; -``` - -### Benefits - -1. **Single Source of Truth** - - Rotation logic (handling RotationMode::Fine, etc.) is in ONE place - - No duplication across UI handlers, commands, and tests - -2. **Type Safety** - - Works through `DocumentContent` abstraction - - Compiler ensures all document types implement required traits - -3. **Future-Proof** - - Adding new document types (DJVU, EPUB) doesn't require updating call sites - - Operations automatically work with new types - -4. **Testable** - - High-level operations can be tested independently - - No UI dependencies - -## Implementation Details - -### How It Works - -High-level operations use the `Transformable` trait: - -```rust -pub fn rotate_document_cw(document: &mut DocumentContent) -> DocResult<()> { - let new_rotation_mode = document.transform_state().rotation.rotate_cw(); - - match new_rotation_mode { - RotationMode::Standard(rot) => document.rotate(rot), - RotationMode::Fine(deg) => { - // Convert fine rotation to nearest 90° standard rotation - // ... - } - } - - Ok(()) -} -``` - -This delegates to the document type's implementation: - -- **Raster:** Actual pixel rotation via `imageops::rotate90()` -- **Vector:** Viewport matrix transformation (lossless!) -- **Portable:** View rotation, rendered by backend (Poppler) - -### Each Type Transforms Differently - -```rust -// Raster: Pixel manipulation (lossy for fine rotations) -impl Transformable for RasterDocument { - fn rotate(&mut self, rotation: Rotation) { - self.image = apply_rotation(self.image, rotation); - } -} - -// Vector: Viewport transform (always lossless!) -impl Transformable for VectorDocument { - fn rotate(&mut self, rotation: Rotation) { - self.transform_matrix = self.transform_matrix.rotate(rotation.to_degrees()); - // No rasterization needed - } -} - -// Portable: View rotation (backend handles rendering) -impl Transformable for PortableDocument { - fn rotate(&mut self, rotation: Rotation) { - self.view_rotation = (self.view_rotation + rotation.to_degrees()) % 360; - } -} -``` - -## Usage Guidelines - -### Prefer High-Level Operations - -```rust -// In application commands -pub fn execute(&self, manager: &mut DocumentManager) -> DocResult<()> { - let document = manager.current_document_mut()?; - transform::rotate_document_cw(document)?; - Ok(()) -} - -// In UI message handlers -AppMessage::RotateCW => { - if let Some(doc) = &mut self.model.document { - transform::rotate_document_cw(doc)?; - } -} -``` - -### Don't Use Low-Level Operations in Application/UI Code - -```rust -// COMPILE ERROR - Low-level operations are pub(crate) -let pixels = transform::apply_rotation(img, Rotation::Cw90); // Won't compile! - -// CORRECT - Use high-level operations -transform::rotate_document_cw(&mut document)?; -``` - -### ℹ️ Low-Level Operations in Document Implementations - -Low-level operations are only accessible within document type implementations: - -```rust -// INTERNAL ONLY - in domain/document/types/raster.rs -impl Transformable for RasterDocument { - fn rotate(&mut self, rotation: Rotation) { - // This works because we're inside the crate - self.image = apply_rotation(self.image, rotation); - } -} -``` - -## Module Structure - -``` -operations/ -├── mod.rs # Public API exports -├── transform.rs # Low-level + High-level transforms -├── render.rs # Rendering utilities (scale, fit, etc.) -├── export.rs # Export to various formats -└── README.md # This file -``` - -## Adding New Operations - -When adding a new operation: - -1. **Add low-level function** (if pixel manipulation is needed) - mark as `pub(crate)` -2. **Add high-level function** that works on `DocumentContent` - mark as `pub` -3. **Export high-level function only** from `mod.rs` -4. **Update domain exports** in `domain/document/mod.rs` -5. **Create command** in `application/commands/` - -Example: - -```rust -// 1. Low-level (internal only) - in transform.rs -pub(crate) fn apply_grayscale(img: DynamicImage) -> DynamicImage { ... } - -// 2. High-level (public API) - in transform.rs -pub fn grayscale_document(document: &mut DocumentContent) -> DocResult<()> { - // Delegates to Transformable trait or uses low-level helper - ... -} - -// 3. Export high-level only - in operations/mod.rs -pub use transform::{grayscale_document}; // NOT apply_grayscale! - -// 4. Export from domain - in document/mod.rs -pub use operations::{grayscale_document}; - -// 5. Command - in application/commands/ -pub struct GrayscaleCommand; -impl GrayscaleCommand { - pub fn execute(&self, manager: &mut DocumentManager) -> DocResult<()> { - let doc = manager.current_document_mut()?; - transform::grayscale_document(doc) // High-level operation - } -} -``` - -## Related Concepts - -- **Traits:** `Renderable`, `Transformable`, `MultiPage` (in `domain/document/core/document.rs`) -- **Type Erasure:** `DocumentContent` enum (in `domain/document/core/content.rs`) -- **Commands:** Application layer operations (in `application/commands/`) -- **Domain Layer:** Pure business logic, no UI dependencies diff --git a/src/domain/document/operations/crop.rs b/src/domain/document/operations/crop.rs deleted file mode 100644 index 8bc0035..0000000 --- a/src/domain/document/operations/crop.rs +++ /dev/null @@ -1,31 +0,0 @@ -// SPDX-License-Identifier: GPL-3.0-or-later -// src/domain/document/operations/crop.rs -// -// Crop operation domain model. - -/// Crop region in pixel coordinates. -/// -/// Pure domain model - represents a rectangular region to crop. -/// No UI concerns, just data. -#[derive(Debug, Clone, Copy, PartialEq, Eq)] -pub struct CropRegion { - pub x: u32, - pub y: u32, - pub width: u32, - pub height: u32, -} - -impl CropRegion { - pub fn new(x: u32, y: u32, width: u32, height: u32) -> Self { - Self { x, y, width, height } - } - - pub fn as_tuple(&self) -> (u32, u32, u32, u32) { - (self.x, self.y, self.width, self.height) - } - - /// Check if region has valid dimensions. - pub fn is_valid(&self) -> bool { - self.width > 0 && self.height > 0 - } -} diff --git a/src/domain/document/operations/export.rs b/src/domain/document/operations/export.rs deleted file mode 100644 index aa78ffa..0000000 --- a/src/domain/document/operations/export.rs +++ /dev/null @@ -1,161 +0,0 @@ -// SPDX-License-Identifier: GPL-3.0-or-later -// src/domain/document/operations/export.rs -// -// Document export operations to various formats. - -use std::path::Path; - -use image::DynamicImage; - -use crate::domain::document::core::document::DocResult; - -/// Supported export formats. -#[derive(Debug, Clone, Copy, PartialEq, Eq)] -pub enum ExportFormat { - /// PNG format (lossless). - Png, - /// JPEG format (lossy). - Jpeg, - /// WebP format. - WebP, - /// PDF format. - Pdf, - /// SVG format (for vector documents). - Svg, -} - -impl ExportFormat { - /// Get file extension for this format. - #[must_use] - pub fn extension(&self) -> &str { - match self { - Self::Png => "png", - Self::Jpeg => "jpg", - Self::WebP => "webp", - Self::Pdf => "pdf", - Self::Svg => "svg", - } - } - - /// Get MIME type for this format. - #[must_use] - pub fn mime_type(&self) -> &str { - match self { - Self::Png => "image/png", - Self::Jpeg => "image/jpeg", - Self::WebP => "image/webp", - Self::Pdf => "application/pdf", - Self::Svg => "image/svg+xml", - } - } - - /// Detect format from file extension. - #[must_use] - pub fn from_path(path: &Path) -> Option { - let ext = path.extension()?.to_str()?.to_lowercase(); - match ext.as_str() { - "png" => Some(Self::Png), - "jpg" | "jpeg" => Some(Self::Jpeg), - "webp" => Some(Self::WebP), - "pdf" => Some(Self::Pdf), - "svg" => Some(Self::Svg), - _ => None, - } - } -} - -/// Export options for image formats. -#[derive(Debug, Clone)] -#[allow(dead_code)] -pub struct ImageExportOptions { - /// Quality setting (0-100) for lossy formats. - pub quality: u8, - /// Whether to preserve metadata (EXIF, etc.). - pub preserve_metadata: bool, -} - -impl Default for ImageExportOptions { - fn default() -> Self { - Self { - quality: 90, - preserve_metadata: true, - } - } -} - -/// Export a raster image to a file. -/// -/// This function handles format-specific encoding and options. -pub fn export_image( - img: &DynamicImage, - path: &Path, - format: ExportFormat, - _options: &ImageExportOptions, -) -> DocResult<()> { - match format { - ExportFormat::Png => { - img.save_with_format(path, image::ImageFormat::Png)?; - } - ExportFormat::Jpeg => { - // TODO: Apply quality settings - img.save_with_format(path, image::ImageFormat::Jpeg)?; - } - ExportFormat::WebP => { - img.save_with_format(path, image::ImageFormat::WebP)?; - } - ExportFormat::Pdf | ExportFormat::Svg => { - return Err(anyhow::anyhow!( - "Export to {} not yet implemented", - format.extension() - )); - } - } - - Ok(()) -} - -/// Export a document to a standard paper format (A4, Letter, etc.). -/// -/// This function resizes the document to fit the target format while maintaining -/// aspect ratio, then exports it. -pub fn export_to_paper_format( - img: &DynamicImage, - path: &Path, - target_width: u32, - target_height: u32, - format: ExportFormat, -) -> DocResult<()> { - use image::imageops::FilterType; - - // Resize to fit target dimensions - let resized = img.resize(target_width, target_height, FilterType::Lanczos3); - - // Export with default options - let options = ImageExportOptions::default(); - export_image(&resized, path, format, &options) -} - -#[cfg(test)] -mod tests { - use super::*; - - #[test] - fn test_format_extension() { - assert_eq!(ExportFormat::Png.extension(), "png"); - assert_eq!(ExportFormat::Jpeg.extension(), "jpg"); - assert_eq!(ExportFormat::Pdf.extension(), "pdf"); - } - - #[test] - fn test_format_from_path() { - assert_eq!( - ExportFormat::from_path(Path::new("test.png")), - Some(ExportFormat::Png) - ); - assert_eq!( - ExportFormat::from_path(Path::new("test.JPG")), - Some(ExportFormat::Jpeg) - ); - assert_eq!(ExportFormat::from_path(Path::new("test.txt")), None); - } -} diff --git a/src/domain/document/operations/mod.rs b/src/domain/document/operations/mod.rs deleted file mode 100644 index 72f7c23..0000000 --- a/src/domain/document/operations/mod.rs +++ /dev/null @@ -1,16 +0,0 @@ -// SPDX-License-Identifier: GPL-3.0-or-later -// src/domain/document/operations/mod.rs -// -// Document operations: transformations, rendering, and export. - -pub mod crop; -pub mod export; -pub mod render; -pub mod transform; - -// Re-export CropRegion for convenience -pub use crop::CropRegion; - -// Note: Low-level pixel operations (apply_rotation, apply_flip, crop_image) -// are internal helpers (pub(crate)) used only by document type implementations. -// Use high-level operations above for application and UI code. diff --git a/src/domain/document/operations/render.rs b/src/domain/document/operations/render.rs deleted file mode 100644 index ce7c0ac..0000000 --- a/src/domain/document/operations/render.rs +++ /dev/null @@ -1,108 +0,0 @@ -// SPDX-License-Identifier: GPL-3.0-or-later -// src/domain/document/operations/render.rs -// -// Rendering operations for documents. - -use cosmic::widget::image::Handle as ImageHandle; -use image::{DynamicImage, GenericImageView}; - -/// Create an image handle from RGBA pixel data. -/// -/// This is the primary way to create image handles for display in the UI. -#[must_use] -pub fn create_image_handle(pixels: Vec, width: u32, height: u32) -> ImageHandle { - ImageHandle::from_rgba(width, height, pixels) -} - -/// Create an image handle from a `DynamicImage`. -/// -/// Converts the image to RGBA8 format and creates a handle. -#[must_use] -pub fn create_image_handle_from_image(img: &DynamicImage) -> ImageHandle { - let (width, height) = img.dimensions(); - let pixels = img.to_rgba8().into_raw(); - create_image_handle(pixels, width, height) -} - -/// Refresh image handle from a `DynamicImage`. -/// -/// Alias for `create_image_handle_from_image` for compatibility. -#[must_use] -pub fn refresh_handle_from_image(img: &DynamicImage) -> ImageHandle { - create_image_handle_from_image(img) -} - -/// Calculate scaled dimensions maintaining aspect ratio. -/// -/// Returns (width, height) scaled by the given factor. -#[must_use] -pub fn scale_dimensions(width: u32, height: u32, scale: f64) -> (u32, u32) { - #[allow(clippy::cast_possible_truncation, clippy::cast_sign_loss)] - let scaled_width = (f64::from(width) * scale).round() as u32; - #[allow(clippy::cast_possible_truncation, clippy::cast_sign_loss)] - let scaled_height = (f64::from(height) * scale).round() as u32; - - (scaled_width.max(1), scaled_height.max(1)) -} - -/// Calculate scale factor to fit dimensions into a target size. -/// -/// Returns a scale factor that will make the image fit within the target -/// dimensions while maintaining aspect ratio. -#[must_use] -pub fn calculate_fit_scale(width: u32, height: u32, target_width: u32, target_height: u32) -> f64 { - if width == 0 || height == 0 { - return 1.0; - } - - let width_scale = f64::from(target_width) / f64::from(width); - let height_scale = f64::from(target_height) / f64::from(height); - - width_scale.min(height_scale) -} - -/// Calculate scale factor to fill dimensions. -/// -/// Returns a scale factor that will make the image fill the target dimensions -/// while maintaining aspect ratio (may crop). -#[must_use] -pub fn calculate_fill_scale(width: u32, height: u32, target_width: u32, target_height: u32) -> f64 { - if width == 0 || height == 0 { - return 1.0; - } - - let width_scale = f64::from(target_width) / f64::from(width); - let height_scale = f64::from(target_height) / f64::from(height); - - width_scale.max(height_scale) -} - -#[cfg(test)] -mod tests { - use super::*; - - #[test] - fn test_scale_dimensions() { - assert_eq!(scale_dimensions(100, 200, 2.0), (200, 400)); - assert_eq!(scale_dimensions(100, 200, 0.5), (50, 100)); - assert_eq!(scale_dimensions(100, 200, 0.0), (1, 1)); // Minimum 1x1 - } - - #[test] - fn test_calculate_fit_scale() { - // Landscape image fitting into square - assert_eq!(calculate_fit_scale(200, 100, 100, 100), 0.5); - // Portrait image fitting into square - assert_eq!(calculate_fit_scale(100, 200, 100, 100), 0.5); - // Square into square - assert_eq!(calculate_fit_scale(100, 100, 100, 100), 1.0); - } - - #[test] - fn test_calculate_fill_scale() { - // Landscape image filling square - assert_eq!(calculate_fill_scale(200, 100, 100, 100), 1.0); - // Portrait image filling square - assert_eq!(calculate_fill_scale(100, 200, 100, 100), 1.0); - } -} diff --git a/src/domain/document/operations/transform.rs b/src/domain/document/operations/transform.rs deleted file mode 100644 index 86a0e0c..0000000 --- a/src/domain/document/operations/transform.rs +++ /dev/null @@ -1,323 +0,0 @@ -// SPDX-License-Identifier: GPL-3.0-or-later -// src/domain/document/operations/transform.rs -// -// Document transformation operations. -// -// This module provides two levels of transformation operations: -// -// 1. **Low-level operations** (internal) for direct pixel manipulation on raster images: -// - `apply_rotation()` - Rotate pixels by 90°, 180°, or 270° [pub(crate)] -// - `apply_flip()` - Flip pixels horizontally or vertically [pub(crate)] -// - `crop_image()` - Crop to a specific region [pub(crate)] -// These are used internally by document type implementations only. -// -// 2. **High-level operations** that work on any document type (raster, vector, PDF): -// - `rotate_document_cw()` - Rotate any document 90° clockwise -// - `rotate_document_ccw()` - Rotate any document 90° counter-clockwise -// - `flip_document_horizontal()` - Flip any document horizontally -// - `flip_document_vertical()` - Flip any document vertically -// - `rotate_document_to()` - Rotate to a specific angle -// - `reset_document_transforms()` - Reset all transformations -// -// ## Usage Example -// -// ```rust -// use crate::domain::document::operations::transform; -// -// // High-level: Works with any DocumentContent (RECOMMENDED) -// let mut document = DocumentContent::Raster(raster_doc); -// transform::rotate_document_cw(&mut document)?; -// transform::flip_document_horizontal(&mut document)?; -// ``` -// -// Note: Low-level operations (apply_rotation, apply_flip, crop_image) are -// internal helpers used by document type implementations and are not part -// of the public API. -// -// The high-level operations use the `Transformable` trait and work across all -// document types (Raster, Vector, Portable), while low-level operations work -// directly on pixel data. - -use image::{DynamicImage, GenericImageView}; - -use crate::domain::document::core::content::DocumentContent; -use crate::domain::document::core::document::{ - DocResult, FlipDirection, Rotation, RotationMode, Transformable, -}; - -/// Apply a 90-degree rotation to a raster image. -/// -/// This function performs the actual pixel manipulation for standard rotations. -/// Used internally by `RasterDocument` implementation. -#[must_use] -pub(crate) fn apply_rotation(img: DynamicImage, rotation: Rotation) -> DynamicImage { - use image::imageops::{rotate180, rotate270, rotate90}; - - match rotation { - Rotation::None => img, - Rotation::Cw90 => DynamicImage::ImageRgba8(rotate90(&img.to_rgba8())), - Rotation::Cw180 => DynamicImage::ImageRgba8(rotate180(&img.to_rgba8())), - Rotation::Cw270 => DynamicImage::ImageRgba8(rotate270(&img.to_rgba8())), - } -} - -/// Apply a flip transformation to a raster image. -/// -/// This function performs the actual pixel manipulation for flip operations. -/// Used internally by `RasterDocument` and `PortableDocument` implementations. -#[must_use] -pub(crate) fn apply_flip(img: DynamicImage, direction: FlipDirection) -> DynamicImage { - use image::imageops::{flip_horizontal, flip_vertical}; - - match direction { - FlipDirection::Horizontal => DynamicImage::ImageRgba8(flip_horizontal(&img.to_rgba8())), - FlipDirection::Vertical => DynamicImage::ImageRgba8(flip_vertical(&img.to_rgba8())), - } -} - -/// Crop a raster image to the specified region. -/// -/// Coordinates are in pixels relative to the top-left corner. -/// Returns None if the crop region is invalid. -/// Used internally for crop operations. -#[must_use] -pub(crate) fn crop_image( - img: &DynamicImage, - x: u32, - y: u32, - width: u32, - height: u32, -) -> Option { - let (img_width, img_height) = img.dimensions(); - - // Validate crop region - if x >= img_width || y >= img_height { - return None; - } - - // Clamp dimensions to image bounds - let crop_width = width.min(img_width - x); - let crop_height = height.min(img_height - y); - - if crop_width == 0 || crop_height == 0 { - return None; - } - - Some(img.crop_imm(x, y, crop_width, crop_height)) -} - -/// Calculate dimensions after rotation. -/// -/// For 90° and 270° rotations, width and height are swapped. -#[must_use] -pub fn dimensions_after_rotation(width: u32, height: u32, rotation: Rotation) -> (u32, u32) { - match rotation { - Rotation::None | Rotation::Cw180 => (width, height), - Rotation::Cw90 | Rotation::Cw270 => (height, width), - } -} - -// ============================================================================ -// High-Level Document Operations (Type-agnostic) -// ============================================================================ -// -// These operations work on ANY document type (Raster, Vector, Portable) through -// the DocumentContent abstraction. They should be preferred over direct trait -// calls when implementing UI commands or application logic. -// -// Benefits: -// - Single API for all document types -// - Handles rotation mode conversions (Standard ↔ Fine) -// - Returns Result for error handling -// - Future-proof for new document types - -/// Rotate a document 90 degrees clockwise. -/// -/// This operation works on any document type (Raster, Vector, Portable) by -/// delegating to the underlying document's `Transformable` implementation. -/// -/// # Examples -/// -/// ```no_run -/// use crate::domain::document::operations::transform::rotate_document_cw; -/// -/// // Works with any document type -/// rotate_document_cw(&mut document)?; -/// ``` -/// -/// # Implementation Details -/// -/// - Raster: Actual pixel rotation via image operations -/// - Vector: Viewport matrix transformation (lossless) -/// - Portable: View rotation, rendered by backend -pub fn rotate_document_cw(document: &mut DocumentContent) -> DocResult<()> { - let new_rotation_mode = document.transform_state().rotation.rotate_cw(); - - match new_rotation_mode { - RotationMode::Standard(rot) => { - document.rotate(rot); - } - RotationMode::Fine(deg) => { - // Convert to nearest 90° rotation - let normalized = ((deg / 90.0).round() as i16 * 90) % 360; - let rot = match normalized { - 0 => Rotation::None, - 90 => Rotation::Cw90, - 180 => Rotation::Cw180, - 270 => Rotation::Cw270, - _ => Rotation::None, - }; - document.rotate(rot); - } - } - - Ok(()) -} - -/// Rotate a document 90 degrees counter-clockwise. -/// -/// This operation works on any document type (Raster, Vector, Portable) by -/// delegating to the underlying document's `Transformable` implementation. -/// -/// # Examples -/// -/// ```no_run -/// use crate::domain::document::operations::transform::rotate_document_ccw; -/// -/// rotate_document_ccw(&mut document)?; -/// ``` -pub fn rotate_document_ccw(document: &mut DocumentContent) -> DocResult<()> { - let new_rotation_mode = document.transform_state().rotation.rotate_ccw(); - - match new_rotation_mode { - RotationMode::Standard(rot) => { - document.rotate(rot); - } - RotationMode::Fine(deg) => { - // Convert to nearest 90° rotation - let normalized = ((deg / 90.0).round() as i16 * 90 + 360) % 360; - let rot = match normalized { - 0 => Rotation::None, - 90 => Rotation::Cw90, - 180 => Rotation::Cw180, - 270 => Rotation::Cw270, - _ => Rotation::None, - }; - document.rotate(rot); - } - } - - Ok(()) -} - -/// Flip a document horizontally (mirror left-right). -/// -/// This operation works on any document type by delegating to the underlying -/// document's `Transformable` implementation. -/// -/// # Examples -/// -/// ```no_run -/// use crate::domain::document::operations::transform::flip_document_horizontal; -/// -/// flip_document_horizontal(&mut document)?; -/// ``` -pub fn flip_document_horizontal(document: &mut DocumentContent) -> DocResult<()> { - document.flip(FlipDirection::Horizontal); - Ok(()) -} - -/// Flip a document vertically (mirror top-bottom). -/// -/// This operation works on any document type by delegating to the underlying -/// document's `Transformable` implementation. -/// -/// # Examples -/// -/// ```no_run -/// use crate::domain::document::operations::transform::flip_document_vertical; -/// -/// flip_document_vertical(&mut document)?; -/// ``` -pub fn flip_document_vertical(document: &mut DocumentContent) -> DocResult<()> { - document.flip(FlipDirection::Vertical); - Ok(()) -} - -/// Rotate a document to a specific angle (0°, 90°, 180°, or 270°). -/// -/// This operation works on any document type by delegating to the underlying -/// document's `Transformable` implementation. -/// -/// # Arguments -/// -/// * `document` - The document to rotate -/// * `rotation` - Target rotation angle -/// -/// # Examples -/// -/// ```no_run -/// use crate::domain::document::core::document::Rotation; -/// use crate::domain::document::operations::transform::rotate_document_to; -/// -/// // Rotate to 180 degrees -/// rotate_document_to(&mut document, Rotation::Cw180)?; -/// ``` -pub fn rotate_document_to(document: &mut DocumentContent, rotation: Rotation) -> DocResult<()> { - document.rotate(rotation); - Ok(()) -} - -/// Reset all transformations on a document. -/// -/// This resets the document to its original state (no rotation, no flips). -/// Useful for implementing "Reset View" functionality. -/// -/// # Examples -/// -/// ```no_run -/// use crate::domain::document::operations::transform::reset_document_transforms; -/// -/// // Undo all rotations and flips -/// reset_document_transforms(&mut document)?; -/// ``` -pub fn reset_document_transforms(document: &mut DocumentContent) -> DocResult<()> { - // Reset to no rotation - document.rotate(Rotation::None); - - // Reset flips by checking current state and flipping back if needed - let state = document.transform_state(); - if state.flip_h { - document.flip(FlipDirection::Horizontal); - } - if state.flip_v { - document.flip(FlipDirection::Vertical); - } - - Ok(()) -} - -#[cfg(test)] -mod tests { - use super::*; - - #[test] - fn test_dimensions_after_rotation() { - assert_eq!( - dimensions_after_rotation(100, 200, Rotation::None), - (100, 200) - ); - assert_eq!( - dimensions_after_rotation(100, 200, Rotation::Cw90), - (200, 100) - ); - assert_eq!( - dimensions_after_rotation(100, 200, Rotation::Cw180), - (100, 200) - ); - assert_eq!( - dimensions_after_rotation(100, 200, Rotation::Cw270), - (200, 100) - ); - } -} diff --git a/src/domain/document/types/mod.rs b/src/domain/document/types/mod.rs deleted file mode 100644 index 07e5470..0000000 --- a/src/domain/document/types/mod.rs +++ /dev/null @@ -1,10 +0,0 @@ -// SPDX-License-Identifier: GPL-3.0-or-later -// src/domain/document/types/mod.rs -// -// Concrete document type implementations. - -pub mod raster; -#[cfg(feature = "vector")] -pub mod vector; -#[cfg(feature = "portable")] -pub mod portable; diff --git a/src/domain/document/types/portable.rs b/src/domain/document/types/portable.rs deleted file mode 100644 index 4d41b5b..0000000 --- a/src/domain/document/types/portable.rs +++ /dev/null @@ -1,473 +0,0 @@ -// SPDX-License-Identifier: GPL-3.0-or-later -// src/domain/document/types/portable.rs -// -// Portable documents (PDF) with poppler backend. - -use std::path::{Path, PathBuf}; - -/// PDF page render quality multiplier (2.0 = double resolution for sharp display). -const PDF_RENDER_QUALITY: f64 = 3.0; - -/// PDF thumbnail size multiplier (0.25 = 25% for fast preview generation). -const PDF_THUMBNAIL_SIZE: f64 = 0.25; - -use cairo::{Context, Format, ImageSurface}; -use image::{DynamicImage, GenericImageView}; -use poppler::PopplerDocument; - -use cosmic::widget::image::Handle as ImageHandle; - -use crate::domain::document::core::document::{ - DocResult, DocumentInfo, FlipDirection, MultiPage, MultiPageThumbnails, Renderable, - RenderOutput, Rotation, RotationMode, TransformState, Transformable, -}; - -/// Represents a portable document (PDF). -pub struct PortableDocument { - /// The parsed PDF document. - document: PopplerDocument, - /// Path to the source file (for caching). - source_path: PathBuf, - /// Total number of pages. - num_pages: usize, - /// Current page index (0-based). - page_index: usize, - /// Current transformation state. - transform: TransformState, - /// Current rendered page as image. - pub rendered: DynamicImage, - /// Image handle for display. - pub handle: ImageHandle, - /// Cached thumbnail handles for each page (None = not yet generated). - thumbnail_cache: Option>, -} - -impl PortableDocument { - /// Open a PDF document and render the first page. - pub fn open(path: &Path) -> anyhow::Result { - let document = PopplerDocument::new_from_file(path, None) - .map_err(|e| anyhow::anyhow!("Failed to parse PDF: {e}"))?; - - let num_pages = document.get_n_pages(); - if num_pages == 0 { - return Err(anyhow::anyhow!("PDF has no pages")); - } - - let rendered = Self::render_page(&document, 0, RotationMode::Standard(Rotation::None))?; - let handle = Self::create_image_handle_from_image(&rendered); - - Ok(Self { - document, - source_path: path.to_path_buf(), - num_pages, - page_index: 0, - transform: TransformState::default(), - rendered, - handle, - thumbnail_cache: None, - }) - } - - /// Returns the current pixel dimensions (width, height). - #[must_use] - pub fn dimensions(&self) -> (u32, u32) { - self.rendered.dimensions() - } - - /// Get the current image handle. - #[must_use] - pub fn handle(&self) -> ImageHandle { - self.handle.clone() - } - - /// Get native dimensions of current page. - #[must_use] - pub fn native_dimensions(&self) -> (u32, u32) { - self.rendered.dimensions() - } - - /// Get the number of thumbnails currently loaded. - pub fn thumbnails_loaded(&self) -> usize { - self.thumbnail_cache.as_ref().map_or(0, Vec::len) - } - - /// Get thumbnail handle for a specific page (read-only access). - /// Returns None if the thumbnail hasn't been generated yet. - #[must_use] - pub fn get_thumbnail_handle(&self, page: usize) -> Option { - self.thumbnail_cache - .as_ref() - .and_then(|cache| cache.get(page).cloned()) - } - - // Helper functions - - /// Extract metadata for this portable document. - pub fn extract_meta(&self, path: &Path) -> crate::domain::document::core::metadata::DocumentMeta { - use crate::domain::document::core::metadata::{BasicMeta, DocumentMeta}; - - let file_name = path - .file_name() - .and_then(|n| n.to_str()) - .unwrap_or("unknown") - .to_string(); - - let file_path = path.to_string_lossy().to_string(); - let file_size = std::fs::metadata(path).map(|m| m.len()).unwrap_or(0); - - let (width, height) = self.dimensions(); - let format = format!("PDF ({} pages)", self.num_pages); - - let basic = BasicMeta { - file_name, - file_path, - format, - width, - height, - file_size, - color_type: "Rendered".to_string(), - }; - - DocumentMeta { basic, exif: None } - } - - /// Crop the current page to the specified rectangle. - /// Works on rendered output (raster). - pub fn crop(&mut self, x: u32, y: u32, width: u32, height: u32) -> Result<(), String> { - let (img_width, img_height) = self.rendered.dimensions(); - - // Validate crop region - if x >= img_width || y >= img_height { - return Err(format!( - "Crop region ({}, {}) is outside rendered bounds ({}, {})", - x, y, img_width, img_height - )); - } - - // Clamp dimensions - let crop_width = width.min(img_width - x); - let crop_height = height.min(img_height - y); - - if crop_width == 0 || crop_height == 0 { - return Err("Crop region has zero width or height".to_string()); - } - - // Crop rendered image - self.rendered = self.rendered.crop_imm(x, y, crop_width, crop_height); - - // Update handle - self.handle = Self::create_image_handle_from_image(&self.rendered); - - Ok(()) - } - fn create_image_handle_from_image(img: &DynamicImage) -> ImageHandle { - let (width, height) = img.dimensions(); - let pixels = img.to_rgba8().into_raw(); - ImageHandle::from_rgba(width, height, pixels) - } - - /// Initialize thumbnail cache (empty, ready for incremental loading). - fn init_thumbnail_cache(&mut self) { - if self.thumbnail_cache.is_none() { - self.thumbnail_cache = Some(Vec::with_capacity(self.num_pages)); - } - } - - /// Generate a single thumbnail page. Returns the next page to generate, or None if done. - pub fn generate_thumbnail_page(&mut self, page: usize) -> Option { - // Initialize cache if needed. - self.init_thumbnail_cache(); - - // Check if we should generate this page. - let should_generate = { - let cache = self.thumbnail_cache.as_ref()?; - page >= cache.len() && page < self.num_pages - }; - - if should_generate { - let handle = self.load_or_generate_thumbnail(page); - if let Some(cache) = self.thumbnail_cache.as_mut() { - cache.push(handle); - } - } - - // Return next page if not done. - let next = page + 1; - if next < self.num_pages { - Some(next) - } else { - None - } - } - - /// Load thumbnail from cache or generate and cache it. - fn load_or_generate_thumbnail(&self, page: usize) -> ImageHandle { - // TODO: Re-enable cache once infrastructure layer is set up - // if let Some(handle) = cache::load_thumbnail(&self.source_path, page) { - // return handle; - // } - - match Self::render_page_at_scale( - &self.document, - page, - RotationMode::Standard(Rotation::None), - PDF_THUMBNAIL_SIZE, - ) { - Ok(img) => { - // TODO: Re-enable cache once infrastructure layer is set up - // let _ = cache::save_thumbnail(&self.source_path, page, &img); - Self::create_image_handle_from_image(&img) - } - Err(e) => { - log::warn!("Failed to generate thumbnail for page {page}: {e}"); - ImageHandle::from_rgba(1, 1, vec![0, 0, 0, 0]) - } - } - } - - /// Render a specific page from the document to an image. - fn render_page( - document: &PopplerDocument, - page_index: usize, - rotation: RotationMode, - ) -> anyhow::Result { - Self::render_page_at_scale(document, page_index, rotation, PDF_RENDER_QUALITY) - } - - /// Render a specific page at a given scale. - fn render_page_at_scale( - document: &PopplerDocument, - page_index: usize, - rotation: RotationMode, - scale: f64, - ) -> anyhow::Result { - let page = document - .get_page(page_index) - .ok_or_else(|| anyhow::anyhow!("Failed to get page {page_index}"))?; - - let (page_width, page_height) = page.get_size(); - let rotation_degrees = rotation.to_degrees() as i16; - - let (width, height) = if rotation_degrees == 90 || rotation_degrees == 270 { - (page_height, page_width) - } else { - (page_width, page_height) - }; - - #[allow(clippy::cast_possible_truncation)] - let scaled_width = (width * scale) as i32; - #[allow(clippy::cast_possible_truncation)] - let scaled_height = (height * scale) as i32; - - let surface = ImageSurface::create(Format::ARgb32, scaled_width, scaled_height) - .map_err(|e| anyhow::anyhow!("Failed to create Cairo surface: {e}"))?; - - let context = Context::new(&surface) - .map_err(|e| anyhow::anyhow!("Failed to create Cairo context: {e}"))?; - - // Fill with white background. - context.set_source_rgb(1.0, 1.0, 1.0); - let _ = context.paint(); - - context.scale(scale, scale); - - if rotation != RotationMode::Standard(Rotation::None) { - let center_x = width / 2.0; - let center_y = height / 2.0; - context.translate(center_x, center_y); - context.rotate(f64::from(rotation_degrees) * std::f64::consts::PI / 180.0); - context.translate(-page_width / 2.0, -page_height / 2.0); - } - - page.render(&context); - - drop(context); - surface.flush(); - - // Direct conversion from Cairo surface to DynamicImage without PNG round-trip. - // This preserves exact pixel data and avoids ARgb32 → PNG → RGBA conversion artifacts. - let width = surface.width() as u32; - let height = surface.height() as u32; - let stride = surface.stride(); - - let data = surface - .take_data() - .map_err(|e| anyhow::anyhow!("Failed to take Cairo surface data: {e}"))?; - - // ARgb32 in Cairo is 4 bytes per pixel (A, R, G, B) with stride padding. - // Convert to standard RGBA by copying pixel-by-pixel, skipping stride padding. - let mut rgba_bytes = Vec::with_capacity((width * 4) as usize); - for y in 0..height { - let row_offset = (y as i32 * stride) as usize; - for x in 0..width { - let col_offset = (x as i32 * 4) as usize; - let idx = row_offset + col_offset; - // Cairo ARgb32: bytes are [A, R, G, B] - let a = data[idx]; - let r = data[idx + 1]; - let g = data[idx + 2]; - let b = data[idx + 3]; - // Output standard RGBA: [R, G, B, A] - rgba_bytes.extend_from_slice(&[r, g, b, a]); - } - } - - Ok(DynamicImage::ImageRgba8(image::RgbaImage::from_raw(width, height, rgba_bytes) - .expect("Failed to create RgbaImage from Cairo surface data"))) - } - - /// Re-render the current page with current transform. - fn rerender(&mut self) { - match Self::render_page(&self.document, self.page_index, self.transform.rotation) { - Ok(mut rendered) => { - // Apply flip transformations to the rendered result - if self.transform.flip_h { - rendered = Self::apply_flip(rendered, FlipDirection::Horizontal); - } - if self.transform.flip_v { - rendered = Self::apply_flip(rendered, FlipDirection::Vertical); - } - self.rendered = rendered; - self.handle = Self::create_image_handle_from_image(&self.rendered); - } - Err(e) => { - log::error!("Failed to render PDF page: {e}"); - } - } - } - - fn apply_flip(img: DynamicImage, direction: FlipDirection) -> DynamicImage { - use image::imageops::{flip_horizontal, flip_vertical}; - match direction { - FlipDirection::Horizontal => DynamicImage::ImageRgba8(flip_horizontal(&img.to_rgba8())), - FlipDirection::Vertical => DynamicImage::ImageRgba8(flip_vertical(&img.to_rgba8())), - } - } - - /// Navigate to the next page. - #[allow(dead_code)] - pub fn next_page(&mut self) -> bool { - if self.page_index + 1 < self.num_pages { - self.page_index += 1; - self.rerender(); - true - } else { - false - } - } - - /// Navigate to the previous page. - #[allow(dead_code)] - pub fn prev_page(&mut self) -> bool { - if self.page_index > 0 { - self.page_index -= 1; - self.rerender(); - true - } else { - false - } - } -} - -// ============================================================================ -// Trait Implementations -// ============================================================================ - -impl Renderable for PortableDocument { - fn render(&mut self, _scale: f64) -> DocResult { - // PDF rendering quality is fixed for now (PDF_RENDER_QUALITY) - let (width, height) = self.dimensions(); - Ok(RenderOutput { - handle: self.handle.clone(), - width, - height, - }) - } - - fn info(&self) -> DocumentInfo { - let (width, height) = self.dimensions(); - DocumentInfo { - width, - height, - format: "PDF".to_string(), - } - } -} - -impl Transformable for PortableDocument { - fn rotate(&mut self, rotation: Rotation) { - self.transform.rotation = RotationMode::Standard(rotation); - self.rerender(); - } - - fn flip(&mut self, direction: FlipDirection) { - match direction { - FlipDirection::Horizontal => self.transform.flip_h = !self.transform.flip_h, - FlipDirection::Vertical => self.transform.flip_v = !self.transform.flip_v, - } - self.rerender(); - } - - fn transform_state(&self) -> TransformState { - self.transform - } -} - -impl MultiPage for PortableDocument { - fn page_count(&self) -> usize { - self.num_pages - } - - fn current_page(&self) -> usize { - self.page_index - } - - fn go_to_page(&mut self, page: usize) -> DocResult<()> { - if page >= self.num_pages { - return Err(anyhow::anyhow!( - "Page {} out of range (0-{})", - page, - self.num_pages - 1 - )); - } - self.page_index = page; - self.rerender(); - Ok(()) - } -} - -impl MultiPageThumbnails for PortableDocument { - fn thumbnails_ready(&self) -> bool { - self.thumbnail_cache - .as_ref() - .is_some_and(|c| c.len() >= self.num_pages) - } - - fn thumbnails_loaded(&self) -> bool { - let loaded = PortableDocument::thumbnails_loaded(self); - loaded >= self.num_pages - } - - fn generate_thumbnail_page(&mut self, page: usize) -> DocResult<()> { - PortableDocument::generate_thumbnail_page(self, page); - Ok(()) - } - - fn generate_all_thumbnails(&mut self) -> DocResult<()> { - if self.thumbnails_ready() { - return Ok(()); - } - self.init_thumbnail_cache(); - for page in 0..self.num_pages { - PortableDocument::generate_thumbnail_page(self, page); - } - Ok(()) - } - - fn get_thumbnail(&mut self, page: usize) -> DocResult> { - Ok(self - .thumbnail_cache - .as_ref() - .and_then(|cache| cache.get(page).cloned())) - } -} diff --git a/src/domain/document/types/raster.rs b/src/domain/document/types/raster.rs deleted file mode 100644 index 85a7027..0000000 --- a/src/domain/document/types/raster.rs +++ /dev/null @@ -1,353 +0,0 @@ -// SPDX-License-Identifier: GPL-3.0-or-later -// src/domain/document/types/raster.rs -// -// Raster image document support (PNG, JPEG, WebP, etc.). - -use std::path::Path; - -use image::{DynamicImage, GenericImageView, ImageReader}; - -use cosmic::widget::image::Handle as ImageHandle; - -use crate::domain::document::core::document::{ - DocResult, DocumentInfo, FlipDirection, InterpolationQuality, Renderable, RenderOutput, - Rotation, RotationMode, TransformState, Transformable, -}; - -/// Represents a raster image document (PNG, JPEG, WebP, ...). -pub struct RasterDocument { - /// The decoded image document. - document: DynamicImage, - /// Native width (original, before transforms). - native_width: u32, - /// Native height (original, before transforms). - native_height: u32, - /// Current transformation state. - transform: TransformState, - /// Cached handle for rendering. - handle: ImageHandle, - /// Accumulated fine rotation angle in degrees. - fine_rotation_angle: f32, - /// Interpolation quality for fine rotation and resize operations. - interpolation_quality: InterpolationQuality, -} - -impl RasterDocument { - /// Load a raster document from disk. - pub fn open(path: &Path) -> image::ImageResult { - let document = ImageReader::open(path)?.decode()?; - let (native_width, native_height) = document.dimensions(); - let handle = Self::create_image_handle_from_image(&document); - - Ok(Self { - document, - native_width, - native_height, - transform: TransformState::default(), - handle, - fine_rotation_angle: 0.0, - interpolation_quality: InterpolationQuality::default(), - }) - } - - /// Returns the current pixel dimensions (width, height) after transforms. - #[must_use] - pub fn dimensions(&self) -> (u32, u32) { - self.document.dimensions() - } - - /// Get the current image handle. - #[must_use] - pub fn handle(&self) -> ImageHandle { - self.handle.clone() - } - - /// Save the current document to disk. - #[allow(dead_code)] - pub fn save(&self, path: &Path) -> image::ImageResult<()> { - self.document.save(path) - } - - /// Get the underlying `DynamicImage`. - #[must_use] - pub fn image(&self) -> &DynamicImage { - &self.document - } - - /// Get native dimensions (before transformations). - #[must_use] - pub fn native_dimensions(&self) -> (u32, u32) { - (self.native_width, self.native_height) - } - - /// Get a reference to the rendered image (for cropping from screen coordinates). - pub fn get_rendered_image(&self) -> &DynamicImage { - &self.document - } - - /// Crop the document to a specified rectangular region (in-place). - /// - /// Coordinates are in pixels relative to the current image dimensions. - /// The crop region is clamped to image bounds if it extends beyond. - /// - /// # Errors - /// - /// Returns an error if the crop region is completely outside the image bounds. - pub fn crop(&mut self, x: u32, y: u32, width: u32, height: u32) -> Result<(), String> { - let (img_width, img_height) = self.document.dimensions(); - - // Validate crop region - if x >= img_width || y >= img_height { - return Err(format!( - "Crop region ({}, {}) is outside image bounds ({}, {})", - x, y, img_width, img_height - )); - } - - // Clamp dimensions to image bounds - let crop_width = width.min(img_width - x); - let crop_height = height.min(img_height - y); - - if crop_width == 0 || crop_height == 0 { - return Err("Crop region has zero width or height".to_string()); - } - - // Apply crop - self.document = self.document.crop_imm(x, y, crop_width, crop_height); - - // Update native dimensions to the cropped size - self.native_width = crop_width; - self.native_height = crop_height; - - // Reset transformations since we have a new "native" image - self.transform = TransformState::default(); - self.fine_rotation_angle = 0.0; - - // Regenerate handle - self.handle = Self::create_image_handle_from_image(&self.document); - - Ok(()) - } - /// Crop the image to the specified rectangle and return as DynamicImage. - /// - /// This does NOT modify the document - it's used for exporting cropped images. - pub fn crop_to_image( - &self, - x: u32, - y: u32, - width: u32, - height: u32, - ) -> Result { - let (img_width, img_height) = self.document.dimensions(); - - // Validate crop region - if x >= img_width || y >= img_height { - return Err(format!( - "Crop rectangle out of bounds: {}x{} at ({}, {}) exceeds image size {}x{}", - width, height, x, y, img_width, img_height - )); - } - - // Clamp dimensions to image bounds - let crop_width = width.min(img_width - x); - let crop_height = height.min(img_height - y); - - if crop_width == 0 || crop_height == 0 { - return Err("Crop region has zero width or height".to_string()); - } - - let cropped = self.document.crop_imm(x, y, crop_width, crop_height); - Ok(cropped) - } - - /// Extract metadata for this raster document. - /// - /// Returns basic metadata (dimensions, format, file size) and EXIF data if available. - pub fn extract_meta( - &self, - path: &Path, - ) -> crate::domain::document::core::metadata::DocumentMeta { - use crate::domain::document::core::metadata::{BasicMeta, DocumentMeta, ExifMeta}; - - let file_name = path - .file_name() - .and_then(|n| n.to_str()) - .unwrap_or("unknown") - .to_string(); - - let file_path = path.to_string_lossy().to_string(); - - let file_size = std::fs::metadata(path).map(|m| m.len()).unwrap_or(0); - - // Detect format from path - let format = path - .extension() - .and_then(|e| e.to_str()) - .unwrap_or("unknown") - .to_uppercase(); - - let color_type = format!("{:?}", self.document.color()); - - let basic = BasicMeta { - file_name, - file_path, - format, - width: self.native_width, - height: self.native_height, - file_size, - color_type, - }; - - // Try to extract EXIF data - let exif = std::fs::read(path) - .ok() - .and_then(|bytes| ExifMeta::from_bytes(&bytes)); - - DocumentMeta { basic, exif } - } - - /// Resize the document to specific dimensions (for format conversion). - /// - /// This is useful for converting images to standard paper formats (A4, US Letter, etc.). - pub fn resize_to_format(&mut self, target_width: u32, target_height: u32) { - use image::imageops::FilterType; - - let filter = match self.interpolation_quality { - InterpolationQuality::Fast => FilterType::Nearest, - InterpolationQuality::Balanced => FilterType::Triangle, - InterpolationQuality::Best => FilterType::CatmullRom, - }; - - self.document = self - .document - .resize_exact(target_width, target_height, filter); - self.handle = Self::create_image_handle_from_image(&self.document); - } - - // Helper functions - fn create_image_handle_from_image(img: &DynamicImage) -> ImageHandle { - let (width, height) = img.dimensions(); - let pixels = img.to_rgba8().into_raw(); - ImageHandle::from_rgba(width, height, pixels) - } - - fn apply_rotation(img: DynamicImage, rotation: Rotation) -> DynamicImage { - use image::imageops::{rotate180, rotate270, rotate90}; - match rotation { - Rotation::None => img, - Rotation::Cw90 => DynamicImage::ImageRgba8(rotate90(&img.to_rgba8())), - Rotation::Cw180 => DynamicImage::ImageRgba8(rotate180(&img.to_rgba8())), - Rotation::Cw270 => DynamicImage::ImageRgba8(rotate270(&img.to_rgba8())), - } - } - - fn apply_flip(img: DynamicImage, direction: FlipDirection) -> DynamicImage { - use image::imageops::{flip_horizontal, flip_vertical}; - match direction { - FlipDirection::Horizontal => DynamicImage::ImageRgba8(flip_horizontal(&img.to_rgba8())), - FlipDirection::Vertical => DynamicImage::ImageRgba8(flip_vertical(&img.to_rgba8())), - } - } -} - -// ============================================================================ -// Trait Implementations -// ============================================================================ - -impl Renderable for RasterDocument { - fn render(&mut self, _scale: f64) -> DocResult { - // Raster images don't re-render at different scales (lossy), - // we just return the current handle. - let (width, height) = self.dimensions(); - Ok(RenderOutput { - handle: self.handle.clone(), - width, - height, - }) - } - - fn info(&self) -> DocumentInfo { - DocumentInfo { - width: self.native_width, - height: self.native_height, - format: "Raster".to_string(), - } - } -} - -impl Transformable for RasterDocument { - fn rotate(&mut self, rotation: Rotation) { - // Extract current rotation in degrees - let current_deg = match self.transform.rotation { - RotationMode::Standard(r) => r.to_degrees(), - RotationMode::Fine(_) => { - // If we have fine rotation, reset it and apply standard rotation - self.fine_rotation_angle = 0.0; - 0 - } - }; - - let new_deg = rotation.to_degrees(); - let diff_deg = (new_deg - current_deg + 360) % 360; - - if diff_deg != 0 { - let rotation_to_apply = match diff_deg { - 90 => Rotation::Cw90, - 180 => Rotation::Cw180, - 270 => Rotation::Cw270, - _ => unreachable!("Invalid rotation diff: {}", diff_deg), - }; - self.document = Self::apply_rotation( - std::mem::replace(&mut self.document, DynamicImage::new_rgb8(1, 1)), - rotation_to_apply, - ); - } - - // Set to standard rotation mode - self.transform.rotation = RotationMode::Standard(rotation); - self.handle = Self::create_image_handle_from_image(&self.document); - } - - fn flip(&mut self, direction: FlipDirection) { - self.document = Self::apply_flip( - std::mem::replace(&mut self.document, DynamicImage::new_rgb8(1, 1)), - direction, - ); - match direction { - FlipDirection::Horizontal => self.transform.flip_h = !self.transform.flip_h, - FlipDirection::Vertical => self.transform.flip_v = !self.transform.flip_v, - } - self.handle = Self::create_image_handle_from_image(&self.document); - } - - fn transform_state(&self) -> TransformState { - self.transform - } - - fn rotate_fine(&mut self, angle_degrees: f32) { - // TODO: Re-enable when imageproc dependency is added to Cargo.toml - // For now, round to nearest 90-degree rotation - log::warn!("Fine rotation not yet implemented, rounding to nearest 90 degrees"); - - let rounded = ((angle_degrees / 90.0).round() as i16 * 90) % 360; - let rotation = match rounded { - 0 => Rotation::None, - 90 => Rotation::Cw90, - 180 => Rotation::Cw180, - 270 => Rotation::Cw270, - _ => Rotation::None, - }; - - self.rotate(rotation); - self.transform.rotation = RotationMode::Standard(rotation); - } - - fn reset_fine_rotation(&mut self) { - self.fine_rotation_angle = 0.0; - self.transform.rotation = RotationMode::Standard(Rotation::None); - } - - fn set_interpolation_quality(&mut self, quality: InterpolationQuality) { - self.interpolation_quality = quality; - } -} diff --git a/src/domain/document/types/vector.rs b/src/domain/document/types/vector.rs deleted file mode 100644 index 2d0c11c..0000000 --- a/src/domain/document/types/vector.rs +++ /dev/null @@ -1,334 +0,0 @@ -// SPDX-License-Identifier: GPL-3.0-or-later -// src/domain/document/types/vector.rs -// -// Vector documents (SVG, etc.). - -use std::path::Path; - -/// Minimum pixmap size for SVG rendering (prevents zero-size pixmaps). -const MIN_PIXMAP_SIZE: u32 = 1; - -use image::{DynamicImage, GenericImageView, RgbaImage}; -use resvg::tiny_skia::{self, Pixmap}; -use resvg::usvg::{Options, Tree}; - -use cosmic::widget::image::Handle as ImageHandle; - -use crate::domain::document::core::document::{ - DocResult, DocumentInfo, FlipDirection, Renderable, RenderOutput, Rotation, RotationMode, - TransformState, Transformable, -}; - -/// Represents a vector document such as SVG. -pub struct VectorDocument { - /// Parsed SVG document for re-rendering at different scales. - document: Tree, - /// Native width of the SVG (from viewBox or width attribute). - native_width: u32, - /// Native height of the SVG (from viewBox or height attribute). - native_height: u32, - /// Current render scale (1.0 = native size). - current_scale: f64, - /// Accumulated transformations. - transform: TransformState, - /// Rasterized image at the current scale. - pub rendered: DynamicImage, - /// Image handle for display. - pub handle: ImageHandle, - /// Current rendered width. - pub width: u32, - /// Current rendered height. - pub height: u32, -} - -impl VectorDocument { - /// Load a vector document from disk. - pub fn open(path: &Path) -> anyhow::Result { - let raw_data = std::fs::read_to_string(path)?; - - // Parse SVG with default options. - let options = Options::default(); - let document = Tree::from_str(&raw_data, &options)?; - - // Get native size from the parsed document. - let size = document.size(); - let native_width = size.width().ceil() as u32; - let native_height = size.height().ceil() as u32; - - let transform = TransformState::default(); - - // Render at native scale (1.0). - let (rendered, width, height) = - render_document(&document, native_width, native_height, 1.0, transform)?; - let handle = Self::create_image_handle_from_image(&rendered); - - Ok(Self { - document, - native_width, - native_height, - current_scale: 1.0, - transform, - rendered, - handle, - width, - height, - }) - } - - /// Returns the dimensions of the rasterized representation. - #[must_use] - pub fn dimensions(&self) -> (u32, u32) { - (self.width, self.height) - } - - /// Get the current image handle. - #[must_use] - pub fn handle(&self) -> ImageHandle { - self.handle.clone() - } - - /// Get native dimensions (before transformations). - #[must_use] - pub fn native_dimensions(&self) -> (u32, u32) { - (self.native_width, self.native_height) - } - - /// Extract metadata for this vector document. - pub fn extract_meta( - &self, - path: &Path, - ) -> crate::domain::document::core::metadata::DocumentMeta { - use crate::domain::document::core::metadata::{BasicMeta, DocumentMeta}; - - let file_name = path - .file_name() - .and_then(|n| n.to_str()) - .unwrap_or("unknown") - .to_string(); - - let file_path = path.to_string_lossy().to_string(); - let file_size = std::fs::metadata(path).map(|m| m.len()).unwrap_or(0); - - let basic = BasicMeta { - file_name, - file_path, - format: "SVG".to_string(), - width: self.native_width, - height: self.native_height, - file_size, - color_type: "Vector".to_string(), - }; - - DocumentMeta { basic, exif: None } - } - - /// Crop the document to the specified rectangle. - /// Works on rendered output (raster). - pub fn crop(&mut self, x: u32, y: u32, width: u32, height: u32) -> Result<(), String> { - let (img_width, img_height) = self.rendered.dimensions(); - - // Validate crop region - if x >= img_width || y >= img_height { - return Err(format!( - "Crop region ({}, {}) is outside rendered bounds ({}, {})", - x, y, img_width, img_height - )); - } - - // Clamp dimensions - let crop_width = width.min(img_width - x); - let crop_height = height.min(img_height - y); - - if crop_width == 0 || crop_height == 0 { - return Err("Crop region has zero width or height".to_string()); - } - - // Crop rendered image - self.rendered = self.rendered.crop_imm(x, y, crop_width, crop_height); - self.width = crop_width; - self.height = crop_height; - - // Update handle - self.handle = Self::create_image_handle_from_image(&self.rendered); - - Ok(()) - } - - /// Re-render the SVG at a new scale, preserving transformations. - /// Returns true if re-rendering occurred. - #[allow(dead_code)] - pub fn render_at_scale(&mut self, scale: f64) -> bool { - // Skip if scale hasn't changed - if (self.current_scale - scale).abs() < f64::EPSILON { - return false; - } - - match render_document( - &self.document, - self.native_width, - self.native_height, - scale, - self.transform, - ) { - Ok((rendered, width, height)) => { - self.current_scale = scale; - self.rendered = rendered; - self.width = width; - self.height = height; - self.handle = Self::create_image_handle_from_image(&self.rendered); - true - } - Err(e) => { - log::error!("Failed to re-render SVG at scale {scale}: {e}"); - false - } - } - } - - /// Re-render with current scale and transform. - fn rerender(&mut self) { - if let Ok((rendered, width, height)) = render_document( - &self.document, - self.native_width, - self.native_height, - self.current_scale, - self.transform, - ) { - self.rendered = rendered; - self.width = width; - self.height = height; - self.handle = Self::create_image_handle_from_image(&self.rendered); - } - } - - // Helper function - fn create_image_handle_from_image(img: &image::DynamicImage) -> ImageHandle { - let (width, height) = img.dimensions(); - let pixels = img.to_rgba8().into_raw(); - ImageHandle::from_rgba(width, height, pixels) - } -} - -// ============================================================================ -// Trait Implementations -// ============================================================================ - -impl Renderable for VectorDocument { - fn render(&mut self, scale: f64) -> DocResult { - self.render_at_scale(scale); - Ok(RenderOutput { - handle: self.handle.clone(), - width: self.width, - height: self.height, - }) - } - - fn info(&self) -> DocumentInfo { - DocumentInfo { - width: self.native_width, - height: self.native_height, - format: "SVG".to_string(), - } - } -} - -impl Transformable for VectorDocument { - fn rotate(&mut self, rotation: Rotation) { - self.transform.rotation = RotationMode::Standard(rotation); - self.rerender(); - } - - fn flip(&mut self, direction: FlipDirection) { - match direction { - FlipDirection::Horizontal => self.transform.flip_h = !self.transform.flip_h, - FlipDirection::Vertical => self.transform.flip_v = !self.transform.flip_v, - } - self.rerender(); - } - - fn transform_state(&self) -> TransformState { - self.transform - } -} - -/// Render the SVG document at a given scale with transformations. -fn render_document( - document: &Tree, - native_width: u32, - native_height: u32, - scale: f64, - transform: TransformState, -) -> anyhow::Result<(DynamicImage, u32, u32)> { - #[allow(clippy::cast_possible_truncation, clippy::cast_sign_loss)] - let width = ((f64::from(native_width) * scale).ceil() as u32).max(MIN_PIXMAP_SIZE); - #[allow(clippy::cast_possible_truncation, clippy::cast_sign_loss)] - let height = ((f64::from(native_height) * scale).ceil() as u32).max(MIN_PIXMAP_SIZE); - - let mut pixmap = - Pixmap::new(width, height).ok_or_else(|| anyhow::anyhow!("Failed to create pixmap"))?; - - #[allow(clippy::cast_possible_truncation)] - let scale_f32 = scale as f32; - let ts = tiny_skia::Transform::from_scale(scale_f32, scale_f32); - resvg::render(document, ts, &mut pixmap.as_mut()); - - let mut image = pixmap_to_dynamic_image(&pixmap); - - // Apply flip transformations using shared utilities - if transform.flip_h { - image = crate::domain::document::operations::transform::apply_flip( - image, - FlipDirection::Horizontal, - ); - } - if transform.flip_v { - image = crate::domain::document::operations::transform::apply_flip( - image, - FlipDirection::Vertical, - ); - } - - // Apply rotation using shared utilities - image = match transform.rotation { - RotationMode::Standard(rotation) => { - crate::domain::document::operations::transform::apply_rotation(image, rotation) - } - RotationMode::Fine(_) => { - // For vector documents, fine rotation is handled differently - // For now, we just render without rotation - // TODO: Implement fine rotation support for vector documents - image - } - }; - - let final_width = image.width(); - let final_height = image.height(); - - Ok((image, final_width, final_height)) -} - -/// Convert a `tiny_skia` Pixmap to a `DynamicImage`. -fn pixmap_to_dynamic_image(pixmap: &Pixmap) -> DynamicImage { - let width = pixmap.width(); - let height = pixmap.height(); - - // tiny_skia uses premultiplied alpha, we need to unpremultiply for image crate - let mut pixels = Vec::with_capacity((width * height * 4) as usize); - for pixel in pixmap.pixels() { - let a = pixel.alpha(); - if a == 0 { - pixels.extend_from_slice(&[0, 0, 0, 0]); - } else { - // Unpremultiply: color = premultiplied_color * 255 / alpha - let r = (u16::from(pixel.red()) * 255 / u16::from(a)) as u8; - let g = (u16::from(pixel.green()) * 255 / u16::from(a)) as u8; - let b = (u16::from(pixel.blue()) * 255 / u16::from(a)) as u8; - pixels.extend_from_slice(&[r, g, b, a]); - } - } - - let rgba_image = RgbaImage::from_raw(width, height, pixels) - .expect("Failed to create RgbaImage from pixmap data"); - - DynamicImage::ImageRgba8(rgba_image) -} diff --git a/src/domain/mod.rs b/src/domain/mod.rs deleted file mode 100644 index c4ad94c..0000000 --- a/src/domain/mod.rs +++ /dev/null @@ -1,20 +0,0 @@ -// SPDX-License-Identifier: GPL-3.0-or-later -// src/domain/mod.rs -// -// Domain layer: business logic, document abstractions, and viewport management. - -pub mod document; - -// Re-export core document types -#[allow(unused_imports)] -pub use document::core::content::DocumentContent; -#[allow(unused_imports)] -pub use document::core::metadata::DocumentMeta; - -// Note: Viewport and error handling were removed to reduce code bloat. -// - Viewport: Was 865 lines of unused code (planned feature) -// - Domain Errors: Not integrated, anyhow::Result is sufficient -// -// Low-level pixel operations (apply_rotation, apply_flip, crop_image) -// are internal helpers used only by document type implementations. -// Use high-level operations for all application and UI code. diff --git a/src/error.rs b/src/error.rs new file mode 100644 index 0000000..402a70a --- /dev/null +++ b/src/error.rs @@ -0,0 +1,46 @@ +// SPDX-License-Identifier: GPL-3.0-or-later +// src/error.rs +// +// Central error and result types for Noctua. + +use std::fmt; + +pub type Result = std::result::Result; + +#[derive(Debug)] +pub enum Error { + /// A requested file or resource was not found. + NotFound(String), + /// The file format or operation is not supported. + Unsupported(String), + /// A session could not be loaded or saved. + Session(String), + /// An underlying I/O error. + Io(std::io::Error), +} + +impl fmt::Display for Error { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + match self { + Error::NotFound(msg) => write!(f, "Not found: {msg}"), + Error::Unsupported(msg) => write!(f, "Unsupported: {msg}"), + Error::Session(msg) => write!(f, "Session error: {msg}"), + Error::Io(err) => write!(f, "I/O error: {err}"), + } + } +} + +impl std::error::Error for Error { + fn source(&self) -> Option<&(dyn std::error::Error + 'static)> { + match self { + Error::Io(err) => Some(err), + _ => None, + } + } +} + +impl From for Error { + fn from(err: std::io::Error) -> Self { + Error::Io(err) + } +} diff --git a/src/i18n.rs b/src/i18n.rs deleted file mode 100644 index 08c2829..0000000 --- a/src/i18n.rs +++ /dev/null @@ -1,55 +0,0 @@ -// SPDX-License-Identifier: GPL-3.0-or-later -// src/i18n.rs -// -// Internationalization (i18n) support. - -use i18n_embed::{ - DefaultLocalizer, LanguageLoader, Localizer, - fluent::{FluentLanguageLoader, fluent_language_loader}, - unic_langid::LanguageIdentifier, -}; -use rust_embed::RustEmbed; -use std::sync::LazyLock; - -/// Applies the requested language(s) to requested translations from the `fl!()` macro. -pub fn init(requested_languages: &[LanguageIdentifier]) { - if let Err(why) = localizer().select(requested_languages) { - println!("error while loading fluent localizations: {why}"); - } -} - -// Get the `Localizer` to be used for localizing this library. -#[must_use] -pub fn localizer() -> Box { - Box::from(DefaultLocalizer::new(&*LANGUAGE_LOADER, &Localizations)) -} - -#[derive(RustEmbed)] -#[folder = "i18n/"] -struct Localizations; - -pub static LANGUAGE_LOADER: LazyLock = LazyLock::new(|| { - let loader: FluentLanguageLoader = fluent_language_loader!(); - - loader - .load_fallback_language(&Localizations) - .expect("Error while loading fallback language"); - - loader -}); - -/// Request a localized string by ID from the i18n/ directory. -#[macro_export] -macro_rules! fl { - ($message_id:literal) => {{ - i18n_embed_fl::fl!($crate::i18n::LANGUAGE_LOADER, $message_id) - }}; - - ($message_id:literal, $($name:ident: $value:expr),*) => {{ - let mut args = std::collections::HashMap::new(); - $( - args.insert(stringify!($name), $value.to_string()); - )* - i18n_embed_fl::fl!($crate::i18n::LANGUAGE_LOADER, $message_id, args) - }}; -} diff --git a/src/infrastructure/cache/mod.rs b/src/infrastructure/cache/mod.rs deleted file mode 100644 index e63c1db..0000000 --- a/src/infrastructure/cache/mod.rs +++ /dev/null @@ -1,9 +0,0 @@ -// SPDX-License-Identifier: GPL-3.0-or-later -// src/infrastructure/cache/mod.rs -// -// Cache infrastructure: thumbnail and document caching. - -pub mod thumbnail_cache; - -// Re-export ThumbnailCache -pub use thumbnail_cache::ThumbnailCache; diff --git a/src/infrastructure/cache/thumbnail_cache.rs b/src/infrastructure/cache/thumbnail_cache.rs deleted file mode 100644 index 5102c54..0000000 --- a/src/infrastructure/cache/thumbnail_cache.rs +++ /dev/null @@ -1,149 +0,0 @@ -// SPDX-License-Identifier: GPL-3.0-or-later -// src/infrastructure/cache/thumbnail_cache.rs -// -// Disk cache for document thumbnails stored in ~/.cache/noctua/ - -use std::fs; -use std::io::BufWriter; -use std::path::{Path, PathBuf}; - -use image::DynamicImage; -use sha2::{Digest, Sha256}; - -use cosmic::widget::image::Handle as ImageHandle; - -use crate::domain::document::operations::render::create_image_handle_from_image; - -/// Cache directory name under ~/.cache/ for thumbnail storage. -const CACHE_DIR: &str = "noctua"; - -/// File extension for cached thumbnails. -const THUMBNAIL_EXT: &str = "png"; - -/// Thumbnail cache manager for disk-based caching. -pub struct ThumbnailCache; - -impl ThumbnailCache { - /// Load a thumbnail from disk cache. - /// Returns None if not cached or cache is invalid. - pub fn load(file_path: &Path, page: usize) -> Option { - let cache_path = Self::thumbnail_path(file_path, page)?; - - log::debug!("Cache lookup: file={}, page={}", file_path.display(), page); - - if !cache_path.exists() { - log::debug!( - "Thumbnail not found in cache: file={} page={}", - file_path.display(), - page - ); - return None; - } - - let img = image::open(&cache_path).ok()?; - log::debug!( - "Thumbnail loaded from cache: file={} page={}", - file_path.display(), - page - ); - Some(create_image_handle_from_image(&img)) - } - - /// Save a thumbnail to disk cache. - pub fn save(file_path: &Path, page: usize, image: &DynamicImage) -> Option<()> { - let dir = Self::ensure_cache_dir()?; - let key = Self::cache_key(file_path, page)?; - let cache_path = dir.join(format!("{key}.{THUMBNAIL_EXT}")); - - log::debug!( - "Saving thumbnail to cache: file={}, page={}, path={}", - file_path.display(), - page, - cache_path.display() - ); - - let file = fs::File::create(&cache_path).ok()?; - let writer = BufWriter::new(file); - - let res = image.write_to( - &mut std::io::BufWriter::new(writer), - image::ImageFormat::Png, - ); - match res { - Ok(()) => { - log::debug!( - "Thumbnail cached successfully: file={} page={}", - file_path.display(), - page - ); - Some(()) - } - Err(e) => { - log::warn!( - "Failed to cache thumbnail: file={} page={}: {}", - file_path.display(), - page, - e - ); - None - } - } - } - - /// Clear all cached thumbnails. - pub fn clear_cache() -> std::io::Result<()> { - if let Some(dir) = Self::cache_dir() - && dir.exists() - { - fs::remove_dir_all(&dir)?; - } - Ok(()) - } - - /// Check if a thumbnail exists in cache. - #[allow(dead_code)] - pub fn has(file_path: &Path, page: usize) -> bool { - Self::thumbnail_path(file_path, page).is_some_and(|p| p.exists()) - } - - // Private helper methods - - /// Get the cache directory path (~/.cache/noctua/). - fn cache_dir() -> Option { - dirs::cache_dir().map(|p| p.join(CACHE_DIR)) - } - - /// Ensure the cache directory exists. - fn ensure_cache_dir() -> Option { - let dir = Self::cache_dir()?; - fs::create_dir_all(&dir).ok()?; - Some(dir) - } - - /// Generate a cache key from file path, modification time, and page number. - /// Format: sha256(path + mtime + page) - fn cache_key(file_path: &Path, page: usize) -> Option { - let metadata = fs::metadata(file_path).ok()?; - let mtime = metadata - .modified() - .ok()? - .duration_since(std::time::UNIX_EPOCH) - .ok()? - .as_secs(); - - let mut hasher = Sha256::new(); - hasher.update(file_path.to_string_lossy().as_bytes()); - hasher.update(mtime.to_le_bytes()); - hasher.update(page.to_le_bytes()); - - let hash = hasher.finalize(); - Some(format!("{hash:x}")) - } - - /// Get the full path for a cached thumbnail. - fn thumbnail_path(file_path: &Path, page: usize) -> Option { - let dir = Self::cache_dir()?; - let key = Self::cache_key(file_path, page)?; - Some(dir.join(format!("{key}.{THUMBNAIL_EXT}"))) - } -} diff --git a/src/infrastructure/filesystem/file_ops.rs b/src/infrastructure/filesystem/file_ops.rs deleted file mode 100644 index 4fce095..0000000 --- a/src/infrastructure/filesystem/file_ops.rs +++ /dev/null @@ -1,189 +0,0 @@ -// SPDX-License-Identifier: GPL-3.0-or-later -// src/infrastructure/filesystem/file_ops.rs -// -// File system operations for document handling. - -use std::fs; -use std::path::{Path, PathBuf}; - -use anyhow::anyhow; - -use crate::domain::document::core::content::{DocumentContent, DocumentKind}; - -use crate::domain::document::types::raster::RasterDocument; -#[cfg(feature = "vector")] -use crate::domain::document::types::vector::VectorDocument; -#[cfg(feature = "portable")] -use crate::domain::document::types::portable::PortableDocument; - -/// Open a document from a file path and dispatch to the correct type. -/// -/// Raster formats are delegated to the `image` crate, which decides -/// based on enabled codecs (e.g. default-formats). -pub fn open_document(path: &Path) -> anyhow::Result { - let kind = DocumentKind::from_path(path) - .ok_or_else(|| anyhow!("Unsupported document type: {}", path.display()))?; - - let content = match kind { - DocumentKind::Raster => { - let raster = RasterDocument::open(path)?; - DocumentContent::Raster(raster) - } - #[cfg(feature = "vector")] - DocumentKind::Vector => { - let vector = VectorDocument::open(path)?; - DocumentContent::Vector(vector) - } - #[cfg(feature = "portable")] - DocumentKind::Portable => { - let portable = PortableDocument::open(path)?; - DocumentContent::Portable(portable) - } - #[cfg(not(any(feature = "vector", feature = "portable")))] - _ => return Err(anyhow!("No document features enabled")), - }; - - Ok(content) -} - -/// Collect all supported document files from a directory, sorted alphabetically. -/// -/// This scans the directory and returns a list of files that are recognized as -/// supported document types (images, PDFs, SVGs, etc.). -pub fn collect_supported_files(dir: &Path) -> Vec { - let mut entries: Vec = Vec::new(); - - if let Ok(read_dir) = fs::read_dir(dir) { - for entry in read_dir.flatten() { - let path = entry.path(); - - // Only keep regular files that are recognized as supported documents. - if path.is_file() && DocumentKind::from_path(&path).is_some() { - entries.push(path); - } - } - } - - entries.sort(); - entries -} - -// --------------------------------------------------------------------------- -// File metadata helpers -// --------------------------------------------------------------------------- - -/// Retrieve the file size in bytes. Returns 0 if the file cannot be accessed. -pub fn file_size(path: &Path) -> u64 { - fs::metadata(path).map(|m| m.len()).unwrap_or(0) -} - -/// Read raw bytes from a file for metadata extraction (e.g., EXIF). -/// Returns None if the file cannot be read. -pub fn read_file_bytes(path: &Path) -> Option> { - fs::read(path).ok() -} - -// --------------------------------------------------------------------------- -// DEPRECATED FUNCTIONS -// --------------------------------------------------------------------------- -// The following functions have been replaced by DocumentManager and are -// commented out to avoid AppModel dependencies. -// -// Instead of using these functions directly, use: -// - DocumentManager::open_document() for opening files -// - DocumentManager::next_document() / previous_document() for navigation -// - Application commands for operations like crop, save, etc. -// --------------------------------------------------------------------------- - -/* -/// Open the initial path passed on the command line. -/// -/// DEPRECATED: Use DocumentManager::open_document() instead. -pub fn open_initial_path(model: &mut AppModel, path: &PathBuf) { - if path.is_dir() { - open_from_directory(model, path); - } else { - open_single_file(model, path); - } -} - -/// Open the first supported document from the given directory. -/// -/// DEPRECATED: Use DocumentManager::open_document() instead. -pub fn open_from_directory(model: &mut AppModel, dir: &Path) { - let entries = collect_supported_files(dir); - - if entries.is_empty() { - model.set_error(format!( - "No supported documents found in directory: {}", - dir.display() - )); - return; - } - - let first = entries[0].clone(); - model.folder_entries = entries; - model.current_index = Some(0); - - load_document_into_model(model, &first); -} - -/// Open a single file. -/// -/// DEPRECATED: Use DocumentManager::open_document() instead. -pub fn open_single_file(model: &mut AppModel, path: &Path) { - load_document_into_model(model, path); - - if model.document.is_some() - && let Some(parent) = path.parent() - { - refresh_folder_entries(model, parent, path); - } -} - -/// Load a document into the model. -/// -/// DEPRECATED: Use DocumentManager methods instead. -fn load_document_into_model(model: &mut AppModel, path: &Path) { - // Implementation omitted - use DocumentManager instead -} - -/// Refresh folder entries. -/// -/// DEPRECATED: DocumentManager handles this automatically. -pub fn refresh_folder_entries(model: &mut AppModel, folder: &Path, current: &Path) { - // Implementation omitted - use DocumentManager instead -} - -/// Navigate to the next document. -/// -/// DEPRECATED: Use DocumentManager::next_document() instead. -pub fn navigate_next(model: &mut AppModel) { - // Implementation omitted - use DocumentManager instead -} - -/// Navigate to the previous document. -/// -/// DEPRECATED: Use DocumentManager::previous_document() instead. -pub fn navigate_prev(model: &mut AppModel) { - // Implementation omitted - use DocumentManager instead -} - -/// Apply crop operation. -/// -/// DEPRECATED: Use CropDocumentCommand instead. -pub fn apply_crop( - crop_selection: &CropSelection, - doc: &DocumentContent, - current_path: &Path, - canvas_size: cosmic::iced::Size, - image_size: cosmic::iced::Size, - scale: f32, - pan_x: f32, - pan_y: f32, - view_mode: &ViewMode, -) -> Result { - // Implementation omitted - use CropDocumentCommand instead - Err("Deprecated function - use CropDocumentCommand".to_string()) -} -*/ diff --git a/src/infrastructure/filesystem/mod.rs b/src/infrastructure/filesystem/mod.rs deleted file mode 100644 index b8a138c..0000000 --- a/src/infrastructure/filesystem/mod.rs +++ /dev/null @@ -1,9 +0,0 @@ -// SPDX-License-Identifier: GPL-3.0-or-later -// src/infrastructure/filesystem/mod.rs -// -// Filesystem operations: file I/O, folder scanning, and file watching. - -pub mod file_ops; - -// TODO: Re-implement these helpers without UI dependencies -// pub use file_ops::{file_size, read_file_bytes}; diff --git a/src/infrastructure/loaders/document_loader.rs b/src/infrastructure/loaders/document_loader.rs deleted file mode 100644 index fb845f5..0000000 --- a/src/infrastructure/loaders/document_loader.rs +++ /dev/null @@ -1,148 +0,0 @@ -// SPDX-License-Identifier: GPL-3.0-or-later -// src/infrastructure/loaders/document_loader.rs -// -// Document loader trait and factory for loading documents from files. - -use std::path::Path; - -use crate::domain::document::core::content::{DocumentContent, DocumentKind}; -use crate::domain::document::core::document::DocResult; - -use super::raster_loader::RasterLoader; -#[cfg(feature = "vector")] -use super::svg_loader::SvgLoader; -#[cfg(feature = "portable")] -use super::pdf_loader::PdfLoader; - -/// Trait for loading documents from files. -/// -/// Implementations handle specific document formats (raster, vector, portable). -pub trait DocumentLoader { - /// Load a document from a file path. - fn load(&self, path: &Path) -> DocResult; - - /// Check if this loader supports the given file. - fn supports(&self, path: &Path) -> bool; -} - -/// Document loader factory. -/// -/// Detects the document format and delegates to the appropriate loader. -pub struct DocumentLoaderFactory; - -impl DocumentLoaderFactory { - /// Create a new document loader factory. - #[must_use] - pub fn new() -> Self { - Self - } - - /// Load a document from a file, automatically detecting the format. - /// - /// # Errors - /// - /// Returns an error if: - /// - The file format is not supported - /// - The file cannot be read - /// - The document is malformed - pub fn load(&self, path: &Path) -> DocResult { - let kind = DocumentKind::from_path(path).ok_or_else(|| { - anyhow::anyhow!( - "Unsupported file format: {}", - path.extension() - .and_then(|e| e.to_str()) - .unwrap_or("unknown") - ) - })?; - - match kind { - DocumentKind::Raster => { - let loader = RasterLoader; - loader.load(path) - } - #[cfg(feature = "vector")] - DocumentKind::Vector => { - let loader = SvgLoader; - loader.load(path) - } - #[cfg(feature = "portable")] - DocumentKind::Portable => { - let loader = PdfLoader; - loader.load(path) - } - #[cfg(not(any(feature = "vector", feature = "portable")))] - _ => Err(anyhow::anyhow!( - "No document loaders available (check feature flags)" - )), - } - } - - /// Detect the document kind from a file path. - #[must_use] - pub fn detect_kind(&self, path: &Path) -> Option { - DocumentKind::from_path(path) - } - - /// Check if a file is supported by any loader. - #[must_use] - pub fn is_supported(&self, path: &Path) -> bool { - DocumentKind::from_path(path).is_some() - } -} - -impl Default for DocumentLoaderFactory { - fn default() -> Self { - Self::new() - } -} - -#[cfg(test)] -mod tests { - use super::*; - - #[test] - fn test_factory_creation() { - let factory = DocumentLoaderFactory::new(); - assert!(std::ptr::eq(&factory, &factory)); // Just a dummy test - } - - #[test] - fn test_detect_kind() { - let factory = DocumentLoaderFactory::new(); - - assert_eq!( - factory.detect_kind(Path::new("test.png")), - Some(DocumentKind::Raster) - ); - assert_eq!( - factory.detect_kind(Path::new("test.jpg")), - Some(DocumentKind::Raster) - ); - - #[cfg(feature = "vector")] - { - assert_eq!( - factory.detect_kind(Path::new("test.svg")), - Some(DocumentKind::Vector) - ); - } - - #[cfg(feature = "portable")] - { - assert_eq!( - factory.detect_kind(Path::new("test.pdf")), - Some(DocumentKind::Portable) - ); - } - - assert_eq!(factory.detect_kind(Path::new("test.txt")), None); - } - - #[test] - fn test_is_supported() { - let factory = DocumentLoaderFactory::new(); - - assert!(factory.is_supported(Path::new("test.png"))); - assert!(!factory.is_supported(Path::new("test.txt"))); - } -} diff --git a/src/infrastructure/loaders/mod.rs b/src/infrastructure/loaders/mod.rs deleted file mode 100644 index 7ebc923..0000000 --- a/src/infrastructure/loaders/mod.rs +++ /dev/null @@ -1,15 +0,0 @@ -// SPDX-License-Identifier: GPL-3.0-or-later -// src/infrastructure/loaders/mod.rs -// -// Document loaders for various formats. - -pub mod document_loader; - -pub mod raster_loader; -#[cfg(feature = "vector")] -pub mod svg_loader; -#[cfg(feature = "portable")] -pub mod pdf_loader; - -// Re-export main types -pub use document_loader::DocumentLoaderFactory; diff --git a/src/infrastructure/loaders/pdf_loader.rs b/src/infrastructure/loaders/pdf_loader.rs deleted file mode 100644 index 7589820..0000000 --- a/src/infrastructure/loaders/pdf_loader.rs +++ /dev/null @@ -1,50 +0,0 @@ -// SPDX-License-Identifier: GPL-3.0-or-later -// src/infrastructure/loaders/pdf_loader.rs -// -// Loader for PDF portable documents. - -use std::path::Path; - -use crate::domain::document::core::content::DocumentContent; -use crate::domain::document::core::document::DocResult; -use crate::domain::document::types::portable::PortableDocument; -use crate::infrastructure::loaders::document_loader::DocumentLoader; - -/// Loader for PDF portable documents. -pub struct PdfLoader; - -impl DocumentLoader for PdfLoader { - fn load(&self, path: &Path) -> DocResult { - let document = PortableDocument::open(path) - .map_err(|e| anyhow::anyhow!("Failed to load PDF document: {e}"))?; - - Ok(DocumentContent::Portable(document)) - } - - fn supports(&self, path: &Path) -> bool { - if let Some(ext) = path.extension() { - let ext_str = ext.to_string_lossy().to_lowercase(); - ext_str == "pdf" - } else { - false - } - } -} - -#[cfg(test)] -mod tests { - use super::*; - - #[test] - fn test_supports() { - let loader = PdfLoader; - - assert!(loader.supports(Path::new("test.pdf"))); - assert!(loader.supports(Path::new("test.PDF"))); - assert!(loader.supports(Path::new("document.pdf"))); - assert!(!loader.supports(Path::new("test.png"))); - assert!(!loader.supports(Path::new("test.svg"))); - assert!(!loader.supports(Path::new("test.jpg"))); - assert!(!loader.supports(Path::new("test.txt"))); - } -} diff --git a/src/infrastructure/loaders/raster_loader.rs b/src/infrastructure/loaders/raster_loader.rs deleted file mode 100644 index 07af388..0000000 --- a/src/infrastructure/loaders/raster_loader.rs +++ /dev/null @@ -1,46 +0,0 @@ -// SPDX-License-Identifier: GPL-3.0-or-later -// src/infrastructure/loaders/raster_loader.rs -// -// Loader for raster image documents (PNG, JPEG, WebP, etc.). - -use std::path::Path; - -use crate::domain::document::core::content::DocumentContent; -use crate::domain::document::core::document::DocResult; -use crate::domain::document::types::raster::RasterDocument; -use crate::infrastructure::loaders::document_loader::DocumentLoader; - -/// Loader for raster image documents. -pub struct RasterLoader; - -impl DocumentLoader for RasterLoader { - fn load(&self, path: &Path) -> DocResult { - let document = RasterDocument::open(path) - .map_err(|e| anyhow::anyhow!("Failed to load raster document: {e}"))?; - - Ok(DocumentContent::Raster(document)) - } - - fn supports(&self, path: &Path) -> bool { - use cosmic::iced_renderer::graphics::image::image_rs::ImageFormat; - - ImageFormat::from_path(path).is_ok() - } -} - -#[cfg(test)] -mod tests { - use super::*; - - #[test] - fn test_supports() { - let loader = RasterLoader; - - assert!(loader.supports(Path::new("test.png"))); - assert!(loader.supports(Path::new("test.jpg"))); - assert!(loader.supports(Path::new("test.jpeg"))); - assert!(loader.supports(Path::new("test.webp"))); - assert!(!loader.supports(Path::new("test.pdf"))); - assert!(!loader.supports(Path::new("test.svg"))); - } -} diff --git a/src/infrastructure/loaders/svg_loader.rs b/src/infrastructure/loaders/svg_loader.rs deleted file mode 100644 index 042887b..0000000 --- a/src/infrastructure/loaders/svg_loader.rs +++ /dev/null @@ -1,49 +0,0 @@ -// SPDX-License-Identifier: GPL-3.0-or-later -// src/infrastructure/loaders/svg_loader.rs -// -// Loader for SVG vector documents. - -use std::path::Path; - -use crate::domain::document::core::content::DocumentContent; -use crate::domain::document::core::document::DocResult; -use crate::domain::document::types::vector::VectorDocument; -use crate::infrastructure::loaders::document_loader::DocumentLoader; - -/// Loader for SVG vector documents. -pub struct SvgLoader; - -impl DocumentLoader for SvgLoader { - fn load(&self, path: &Path) -> DocResult { - let document = VectorDocument::open(path) - .map_err(|e| anyhow::anyhow!("Failed to load SVG document: {e}"))?; - - Ok(DocumentContent::Vector(document)) - } - - fn supports(&self, path: &Path) -> bool { - if let Some(ext) = path.extension() { - let ext_str = ext.to_string_lossy().to_lowercase(); - ext_str == "svg" || ext_str == "svgz" - } else { - false - } - } -} - -#[cfg(test)] -mod tests { - use super::*; - - #[test] - fn test_supports() { - let loader = SvgLoader; - - assert!(loader.supports(Path::new("test.svg"))); - assert!(loader.supports(Path::new("test.SVG"))); - assert!(loader.supports(Path::new("test.svgz"))); - assert!(!loader.supports(Path::new("test.png"))); - assert!(!loader.supports(Path::new("test.pdf"))); - assert!(!loader.supports(Path::new("test.jpg"))); - } -} diff --git a/src/infrastructure/mod.rs b/src/infrastructure/mod.rs deleted file mode 100644 index 478c9c3..0000000 --- a/src/infrastructure/mod.rs +++ /dev/null @@ -1,13 +0,0 @@ -// SPDX-License-Identifier: GPL-3.0-or-later -// src/infrastructure/mod.rs -// -// Infrastructure layer: external dependencies, loaders, cache, and filesystem. - -pub mod cache; -pub mod filesystem; -pub mod loaders; -pub mod system; - -// Re-export loader factory -#[allow(unused_imports)] -pub use loaders::DocumentLoaderFactory; diff --git a/src/infrastructure/system/mod.rs b/src/infrastructure/system/mod.rs deleted file mode 100644 index 7a1068a..0000000 --- a/src/infrastructure/system/mod.rs +++ /dev/null @@ -1,9 +0,0 @@ -// SPDX-License-Identifier: GPL-3.0-or-later -// src/infrastructure/system/mod.rs -// -// System integration: wallpaper, desktop environment utilities. - -pub mod wallpaper; - -// Re-export wallpaper function -pub use wallpaper::set_as_wallpaper; diff --git a/src/infrastructure/system/wallpaper.rs b/src/infrastructure/system/wallpaper.rs deleted file mode 100644 index 6ac377f..0000000 --- a/src/infrastructure/system/wallpaper.rs +++ /dev/null @@ -1,159 +0,0 @@ -// SPDX-License-Identifier: GPL-3.0-or-later -// src/infrastructure/system/wallpaper.rs -// -// Set desktop wallpaper across different desktop environments. - -use std::path::Path; - -/// Set an image as desktop wallpaper using multiple fallback methods. -/// -/// Attempts the following methods in order: -/// 1. COSMIC Desktop (direct config file modification) -/// 2. wallpaper crate (KDE, XFCE, Windows, macOS) -/// 3. gsettings (GNOME) -/// 4. feh (tiling window managers) -pub fn set_as_wallpaper(path: &Path) { - // Canonicalize to absolute path. - let abs_path = match path.canonicalize() { - Ok(p) => p, - Err(e) => { - log::error!("Failed to canonicalize path {}: {}", path.display(), e); - return; - } - }; - - let Some(path_str) = abs_path.to_str() else { - log::error!("Invalid UTF-8 in path: {}", abs_path.display()); - return; - }; - - log::info!("Attempting to set wallpaper: {path_str}"); - - // Method 1: Try COSMIC Desktop (direct config file modification). - if try_cosmic_wallpaper(path_str) { - return; - } - - // Method 2: Try wallpaper crate (supports KDE, XFCE, Windows, macOS). - if try_wallpaper_crate(path_str) { - return; - } - - // Method 3: Try GNOME via gsettings. - if try_gsettings_wallpaper(path_str) { - return; - } - - // Method 4: Try feh (common on tiling WMs like i3, sway). - if try_feh_wallpaper(path_str) { - return; - } - - log::error!("All methods failed to set wallpaper"); -} - -/// Try setting wallpaper via COSMIC config file. -fn try_cosmic_wallpaper(path_str: &str) -> bool { - let Some(home) = dirs::home_dir() else { - return false; - }; - - let cosmic_config = home.join(".config/cosmic/com.system76.CosmicBackground/v1/all"); - if !cosmic_config.exists() { - return false; - } - - let config_content = format!( - r#"( - output: "all", - source: Path("{path_str}"), - filter_by_theme: true, - rotation_frequency: 300, - filter_method: Lanczos, - scaling_mode: Zoom, - sampling_method: Alphanumeric, -)"# - ); - - match std::fs::write(&cosmic_config, config_content) { - Ok(()) => { - log::info!("Wallpaper set via COSMIC config"); - true - } - Err(e) => { - log::warn!("Failed to write COSMIC config: {e}"); - false - } - } -} - -/// Try setting wallpaper via wallpaper crate. -fn try_wallpaper_crate(path_str: &str) -> bool { - match wallpaper::set_from_path(path_str) { - Ok(()) => { - log::info!("Wallpaper set via wallpaper crate"); - true - } - Err(e) => { - log::warn!("wallpaper crate failed: {e}"); - false - } - } -} - -/// Try setting wallpaper via GNOME gsettings. -fn try_gsettings_wallpaper(path_str: &str) -> bool { - let uri = format!("file://{path_str}"); - - let output = match std::process::Command::new("gsettings") - .args(["set", "org.gnome.desktop.background", "picture-uri", &uri]) - .output() - { - Ok(o) => o, - Err(e) => { - log::warn!("gsettings command failed: {e}"); - return false; - } - }; - - if !output.status.success() { - log::warn!( - "gsettings failed: {}", - String::from_utf8_lossy(&output.stderr) - ); - return false; - } - - log::info!("Wallpaper set via gsettings"); - - // Also set dark mode wallpaper. - let _ = std::process::Command::new("gsettings") - .args([ - "set", - "org.gnome.desktop.background", - "picture-uri-dark", - &uri, - ]) - .output(); - - true -} - -/// Try setting wallpaper via feh. -fn try_feh_wallpaper(path_str: &str) -> bool { - let Ok(output) = std::process::Command::new("feh") - .args(["--bg-scale", path_str]) - .output() - else { - log::warn!("feh not available"); - return false; - }; - - if output.status.success() { - log::info!("Wallpaper set via feh"); - true - } else { - log::warn!("feh failed"); - false - } -} diff --git a/src/main.rs b/src/main.rs index aa1164a..0478017 100644 --- a/src/main.rs +++ b/src/main.rs @@ -1,43 +1,734 @@ // SPDX-License-Identifier: GPL-3.0-or-later // src/main.rs // -// Application entry point. +// Minimal COSMIC viewer entry point for images and PDFs. -mod ui; -mod application; -mod domain; -mod infrastructure; - -mod config; -mod i18n; - -use anyhow::Result; use clap::Parser; -use cosmic::app::Settings; -use crate::ui::NoctuaApp; +use cosmic::app::{Core, Settings, Task}; +use cosmic::iced::{ + event, + keyboard::{self, key::Named, Key}, + mouse, Alignment, ContentFit, Event, Length, Limits, Subscription, +}; +use cosmic::{executor, theme, widget, Application, Element}; +use std::collections::hash_map::DefaultHasher; +use std::ffi::OsStr; +use std::hash::{Hash, Hasher}; +use std::path::{Path, PathBuf}; +use std::process::Command; -#[derive(Parser, Debug, Clone)] -#[command(version, about)] -pub struct Args { - /// File to open on startup +#[derive(Parser, Debug)] +#[command( + name = "noctua", + version, + about = "A document and image viewer for COSMIC" +)] +struct Args { + /// Files to open. #[arg(value_name = "FILE")] - pub file: Option, + files: Vec, - /// UI language (e.g. "en", "de") - #[arg(short, long, default_value = "en")] - pub language: String, + /// Compatibility with the previous CLI harness. + #[arg(long = "open-document", value_name = "PATH")] + document: Option, + + /// Open all supported documents in a directory. + #[arg(long = "open-directory", value_name = "PATH")] + directory: Option, } -fn main() -> Result<()> { - // Get the system's preferred languages. - let requested_languages = i18n_embed::DesktopLanguageRequester::requested_languages(); +#[derive(Clone, Debug, Eq, PartialEq)] +enum DocumentKind { + Raster, + Svg, + Pdf, +} - // Enable localizations to be applied. - i18n::init(&requested_languages); +#[derive(Clone, Debug)] +struct Document { + path: PathBuf, + kind: DocumentKind, + page: usize, + page_count: Option, + rendered_page: Option, + rendering_page: Option, + prefetching_pages: Vec, + error: Option, +} + +impl Document { + fn new(path: PathBuf, kind: DocumentKind) -> Self { + let page_count = (kind == DocumentKind::Pdf) + .then(|| pdf_page_count(&path)) + .flatten(); + + Self { + path, + kind, + page: 0, + page_count, + rendered_page: None, + rendering_page: None, + prefetching_pages: Vec::new(), + error: None, + } + } + + fn name(&self) -> String { + self.path + .file_name() + .and_then(OsStr::to_str) + .unwrap_or("Noctua") + .to_string() + } +} + +#[derive(Clone, Debug)] +struct Flags { + documents: Vec, + initial_index: usize, +} + +#[derive(Debug, Clone)] +enum Message { + PreviousDocument, + NextDocument, + PreviousPage, + NextPage, + ZoomIn, + ZoomOut, + ResetZoom, + ModifiersChanged(keyboard::Modifiers), + Scroll(f32), + PdfRendered { + path: PathBuf, + page: usize, + result: Result, + }, + PdfPrefetched { + path: PathBuf, + page: usize, + result: Result, + }, +} + +struct NoctuaApp { + core: Core, + documents: Vec, + current: usize, + zoom: f32, + modifiers: keyboard::Modifiers, +} + +fn main() -> cosmic::iced::Result { + env_logger::Builder::from_env(env_logger::Env::default().default_filter_or("warn")).init(); - env_logger::init(); let args = Args::parse(); + let flags = Flags::from_args(args); - cosmic::app::run::(Settings::default(), ui::app::Flags::Args(args)) - .map_err(|e| anyhow::anyhow!(e)) + let settings = Settings::default().size_limits(Limits::NONE.min_width(480.0).min_height(320.0)); + + cosmic::app::run::(settings, flags) +} + +impl Flags { + fn from_args(args: Args) -> Self { + let mut requested = args.files; + + if let Some(path) = args.document { + requested.push(path); + } + + if let Some(path) = args.directory { + requested.push(path); + } + + let (documents, initial_index) = collect_documents(&requested); + + Self { + documents, + initial_index, + } + } +} + +impl Application for NoctuaApp { + type Executor = executor::Default; + type Flags = Flags; + type Message = Message; + + const APP_ID: &'static str = "org.codeberg.wfx.Noctua"; + + fn core(&self) -> &Core { + &self.core + } + + fn core_mut(&mut self) -> &mut Core { + &mut self.core + } + + fn init(mut core: Core, flags: Self::Flags) -> (Self, Task) { + core.window.content_container = false; + + let current = flags + .initial_index + .min(flags.documents.len().saturating_sub(1)); + + let mut app = Self { + core, + documents: flags.documents, + current, + zoom: 1.0, + modifiers: keyboard::Modifiers::default(), + }; + + app.refresh_title(); + let task = app.ensure_current_rendered(); + + (app, task) + } + + fn update(&mut self, message: Self::Message) -> Task { + match message { + Message::PreviousDocument => { + if self.current > 0 { + self.current -= 1; + self.zoom = 1.0; + } + } + Message::NextDocument => { + if self.current + 1 < self.documents.len() { + self.current += 1; + self.zoom = 1.0; + } + } + Message::PreviousPage => { + self.previous_pdf_page(); + } + Message::NextPage => { + self.next_pdf_page(); + } + Message::ZoomIn => { + self.zoom_in(); + } + Message::ZoomOut => { + self.zoom_out(); + } + Message::ResetZoom => { + self.zoom = 1.0; + } + Message::ModifiersChanged(modifiers) => { + self.modifiers = modifiers; + } + Message::Scroll(delta) => { + if self.modifiers.control() { + if delta > 0.0 { + self.zoom_in(); + } else if delta < 0.0 { + self.zoom_out(); + } + } else if delta > 0.0 { + self.previous_pdf_page(); + } else if delta < 0.0 { + self.next_pdf_page(); + } + } + Message::PdfRendered { path, page, result } => { + if let Some(document) = self + .documents + .iter_mut() + .find(|document| document.path == path) + { + if document.rendering_page == Some(page) { + document.rendering_page = None; + } + + if document.page == page { + match result { + Ok(rendered) => { + document.rendered_page = Some(rendered); + document.error = None; + } + Err(err) => { + document.rendered_page = None; + document.error = Some(err); + } + } + } + } + } + Message::PdfPrefetched { path, page, result } => { + if let Some(document) = self + .documents + .iter_mut() + .find(|document| document.path == path) + { + document + .prefetching_pages + .retain(|pending| *pending != page); + + if document.page == page && document.rendered_page.is_none() { + if let Ok(rendered) = result { + document.rendered_page = Some(rendered); + document.error = None; + } + } + } + } + } + + self.refresh_title(); + + self.ensure_current_rendered() + } + + fn subscription(&self) -> Subscription { + event::listen_raw(|event, status, _| match event { + Event::Keyboard(keyboard::Event::ModifiersChanged(modifiers)) => { + Some(Message::ModifiersChanged(modifiers)) + } + Event::Keyboard(keyboard::Event::KeyPressed { + key: Key::Named(Named::PageUp | Named::ArrowUp | Named::ArrowLeft), + .. + }) if status == event::Status::Ignored => Some(Message::PreviousPage), + Event::Keyboard(keyboard::Event::KeyPressed { + key: Key::Named(Named::PageDown | Named::ArrowDown | Named::ArrowRight), + .. + }) if status == event::Status::Ignored => Some(Message::NextPage), + Event::Mouse(mouse::Event::WheelScrolled { delta }) + if status == event::Status::Ignored => + { + scroll_delta_y(delta).map(Message::Scroll) + } + _ => None, + }) + } + + fn view(&self) -> Element<'_, Self::Message> { + let content = match self.current_document() { + Some(document) => self.document_view(document), + None => empty_view("No document selected"), + }; + + widget::container(content) + .width(Length::Fill) + .height(Length::Fill) + .center_x(Length::Fill) + .center_y(Length::Fill) + .class(theme::Container::WindowBackground) + .into() + } + + fn footer(&self) -> Option> { + let Some(document) = self.current_document() else { + return None; + }; + + let has_previous_document = self.current > 0; + let has_next_document = self.current + 1 < self.documents.len(); + let has_previous_page = document.kind == DocumentKind::Pdf && document.page > 0; + let has_next_page = document.kind == DocumentKind::Pdf + && document + .page_count + .is_none_or(|page_count| document.page + 1 < page_count); + + let page_label = if document.kind == DocumentKind::Pdf { + match document.page_count { + Some(page_count) => format!("Page {} / {}", document.page + 1, page_count), + None => format!("Page {}", document.page + 1), + } + } else { + format!("{} / {}", self.current + 1, self.documents.len()) + }; + + let row = widget::row::with_capacity(9) + .align_y(Alignment::Center) + .spacing(8) + .padding(8) + .push( + widget::button::standard("Previous") + .on_press_maybe(has_previous_document.then_some(Message::PreviousDocument)), + ) + .push( + widget::button::standard("Next") + .on_press_maybe(has_next_document.then_some(Message::NextDocument)), + ) + .push(widget::space::horizontal().width(Length::Fill)) + .push( + widget::button::standard("-") + .on_press(Message::ZoomOut) + .width(Length::Fixed(36.0)), + ) + .push(widget::button::standard("100%").on_press(Message::ResetZoom)) + .push( + widget::button::standard("+") + .on_press(Message::ZoomIn) + .width(Length::Fixed(36.0)), + ) + .push(widget::space::horizontal().width(Length::Fill)) + .push(widget::text::body(page_label)) + .push( + widget::button::standard("Prev page") + .on_press_maybe(has_previous_page.then_some(Message::PreviousPage)), + ) + .push( + widget::button::standard("Next page") + .on_press_maybe(has_next_page.then_some(Message::NextPage)), + ); + + Some(row.into()) + } +} + +impl NoctuaApp { + fn zoom_in(&mut self) { + self.zoom = (self.zoom * 1.2).min(5.0); + } + + fn zoom_out(&mut self) { + self.zoom = (self.zoom / 1.2).max(0.2); + } + + fn previous_pdf_page(&mut self) { + if let Some(document) = self.current_document_mut() { + if document.kind == DocumentKind::Pdf && document.page > 0 { + document.page -= 1; + document.rendered_page = None; + document.error = None; + } + } + } + + fn next_pdf_page(&mut self) { + if let Some(document) = self.current_document_mut() { + if document.kind == DocumentKind::Pdf { + let page_count = document.page_count.unwrap_or(document.page + 2); + if document.page + 1 < page_count { + document.page += 1; + document.rendered_page = None; + document.error = None; + } + } + } + } + + fn current_document(&self) -> Option<&Document> { + self.documents.get(self.current) + } + + fn current_document_mut(&mut self) -> Option<&mut Document> { + self.documents.get_mut(self.current) + } + + fn refresh_title(&mut self) { + let title = self + .current_document() + .map_or_else(|| "Noctua".to_string(), Document::name); + + self.core.set_header_title(title); + } + + fn ensure_current_rendered(&mut self) -> Task { + let render_task = { + let Some(document) = self.current_document_mut() else { + return Task::none(); + }; + + if document.kind != DocumentKind::Pdf { + return Task::none(); + } + + if document.rendered_page.is_some() { + return self.ensure_adjacent_rendered(); + } + + if document.rendering_page == Some(document.page) { + return Task::none(); + } + + if let Some(cached) = cached_pdf_page(&document.path, document.page) { + document.rendered_page = Some(cached); + document.error = None; + document.rendering_page = None; + return self.ensure_adjacent_rendered(); + } + + document.rendering_page = Some(document.page); + document.error = None; + + render_pdf_task( + document.path.clone(), + document.page, + |path, page, result| Message::PdfRendered { path, page, result }, + ) + }; + + Task::batch([render_task, self.ensure_adjacent_rendered()]) + } + + fn ensure_adjacent_rendered(&mut self) -> Task { + let Some(document) = self.current_document_mut() else { + return Task::none(); + }; + + if document.kind != DocumentKind::Pdf || document.rendered_page.is_none() { + return Task::none(); + } + + let mut pages = Vec::with_capacity(2); + if document + .page_count + .is_none_or(|page_count| document.page + 1 < page_count) + { + pages.push(document.page + 1); + } + if document.page > 0 { + pages.push(document.page - 1); + } + + let path = document.path.clone(); + let pages = pages + .into_iter() + .filter(|page| document.rendering_page != Some(*page)) + .filter(|page| !document.prefetching_pages.contains(page)) + .filter(|page| cached_pdf_page(&path, *page).is_none()) + .collect::>(); + + document.prefetching_pages.extend(pages.iter().copied()); + + let tasks = pages + .into_iter() + .map(|page| { + render_pdf_task(path.clone(), page, |path, page, result| { + Message::PdfPrefetched { path, page, result } + }) + }) + .collect::>(); + + Task::batch(tasks) + } + + fn document_view(&self, document: &Document) -> Element<'_, Message> { + if let Some(error) = &document.error { + return empty_view(error); + } + + match document.kind { + DocumentKind::Raster => widget::image(widget::image::Handle::from_path(&document.path)) + .content_fit(ContentFit::Contain) + .width(Length::Fill) + .height(Length::Fill) + .scale(self.zoom) + .into(), + DocumentKind::Svg => widget::svg(widget::svg::Handle::from_path(&document.path)) + .content_fit(ContentFit::Contain) + .width(Length::Fill) + .height(Length::Fill) + .into(), + DocumentKind::Pdf => match &document.rendered_page { + Some(path) => widget::image(widget::image::Handle::from_path(path)) + .content_fit(ContentFit::Contain) + .width(Length::Fill) + .height(Length::Fill) + .scale(self.zoom) + .into(), + None => empty_view("Rendering PDF page..."), + }, + } + } +} + +fn empty_view<'a>(message: impl Into) -> Element<'a, Message> { + widget::column::with_capacity(3) + .align_x(Alignment::Center) + .spacing(8) + .push(widget::space::vertical()) + .push(widget::icon::from_name("image-x-generic-symbolic").size(64)) + .push(widget::text::body(message.into())) + .push(widget::space::vertical()) + .width(Length::Fill) + .height(Length::Fill) + .into() +} + +fn scroll_delta_y(delta: mouse::ScrollDelta) -> Option { + let y = match delta { + mouse::ScrollDelta::Lines { y, .. } => y, + mouse::ScrollDelta::Pixels { y, .. } => y / 60.0, + }; + + (y.abs() > f32::EPSILON).then_some(y) +} + +fn collect_documents(requested: &[PathBuf]) -> (Vec, usize) { + if requested.is_empty() { + return (Vec::new(), 0); + } + + if requested.len() == 1 { + let path = &requested[0]; + + if path.is_dir() { + return (scan_directory(path), 0); + } + + if let Some(parent) = path.parent() { + let documents = scan_directory(parent); + if let Some(index) = documents + .iter() + .position(|document| same_path(&document.path, path)) + { + return (documents, index); + } + } + } + + let documents = requested + .iter() + .flat_map(|path| { + if path.is_dir() { + scan_directory(path) + } else { + detect_kind(path) + .map(|kind| vec![Document::new(path.clone(), kind)]) + .unwrap_or_default() + } + }) + .collect(); + + (documents, 0) +} + +fn scan_directory(dir: &Path) -> Vec { + let Ok(entries) = std::fs::read_dir(dir) else { + return Vec::new(); + }; + + let mut paths = entries + .filter_map(Result::ok) + .map(|entry| entry.path()) + .filter(|path| path.is_file()) + .filter_map(|path| detect_kind(&path).map(|kind| (path, kind))) + .collect::>(); + + paths.sort_by(|(left, _), (right, _)| left.cmp(right)); + + paths + .into_iter() + .map(|(path, kind)| Document::new(path, kind)) + .collect() +} + +fn detect_kind(path: &Path) -> Option { + match path + .extension() + .and_then(OsStr::to_str) + .map(str::to_ascii_lowercase) + .as_deref() + { + Some("pdf") => Some(DocumentKind::Pdf), + Some("svg") => Some(DocumentKind::Svg), + Some("png" | "jpg" | "jpeg" | "webp" | "gif" | "bmp" | "tiff" | "tif") => { + Some(DocumentKind::Raster) + } + _ => None, + } +} + +fn same_path(left: &Path, right: &Path) -> bool { + left == right || left.canonicalize().ok().as_deref() == right.canonicalize().ok().as_deref() +} + +fn pdf_page_count(path: &Path) -> Option { + let output = Command::new("pdfinfo").arg(path).output().ok()?; + + if !output.status.success() { + return None; + } + + let stdout = String::from_utf8_lossy(&output.stdout); + stdout.lines().find_map(|line| { + line.strip_prefix("Pages:") + .and_then(|value| value.trim().parse::().ok()) + }) +} + +fn render_pdf_task( + path: PathBuf, + page: usize, + on_done: impl FnOnce(PathBuf, usize, Result) -> Message + Send + 'static, +) -> Task { + Task::perform( + async move { + let result = render_pdf_page(&path, page); + (path, page, result) + }, + move |(path, page, result)| cosmic::Action::App(on_done(path, page, result)), + ) +} + +fn cached_pdf_page(path: &Path, page: usize) -> Option { + let output = pdf_page_output(path, page); + output.exists().then_some(output) +} + +fn pdf_page_output(path: &Path, page: usize) -> PathBuf { + std::env::temp_dir() + .join(format!( + "noctua-pdf-{}-{}", + path_hash(path), + page.saturating_add(1) + )) + .with_extension("png") +} + +fn render_pdf_page(path: &Path, page: usize) -> Result { + if let Some(cached) = cached_pdf_page(path, page) { + return Ok(cached); + } + + let output = pdf_page_output(path, page); + let prefix = output.with_extension(""); + + let status = Command::new("pdftocairo") + .arg("-png") + .arg("-singlefile") + .arg("-f") + .arg(page.saturating_add(1).to_string()) + .arg("-l") + .arg(page.saturating_add(1).to_string()) + .arg("-scale-to") + .arg("2400") + .arg(path) + .arg(&prefix) + .status() + .map_err(|err| format!("Failed to run pdftocairo: {err}"))?; + + if status.success() && output.exists() { + Ok(output) + } else { + Err(format!( + "Failed to render page {} of {}", + page.saturating_add(1), + path.display() + )) + } +} + +fn path_hash(path: &Path) -> u64 { + let mut hasher = DefaultHasher::new(); + + match path.canonicalize() { + Ok(path) => path.hash(&mut hasher), + Err(_) => path.hash(&mut hasher), + } + + if let Ok(metadata) = path.metadata() { + metadata.len().hash(&mut hasher); + if let Ok(modified) = metadata.modified() { + modified.hash(&mut hasher); + } + } + + hasher.finish() } diff --git a/src/patch.rs b/src/patch.rs new file mode 100644 index 0000000..2e44fcd --- /dev/null +++ b/src/patch.rs @@ -0,0 +1,338 @@ +// SPDX-License-Identifier: GPL-3.0-or-later +// src/patch.rs +// +// Patch file executor for scripted workspace operations and testing. + +use crate::active_state::ActiveState; +use crate::workspace::model::{Item, ItemKind, Session, SessionType, Workspace}; +use anyhow::{bail, Context, Result}; +use serde::Deserialize; +use std::path::{Path, PathBuf}; + +/// A single step in a patch file. Each variant maps directly to a JSON key. +#[derive(Debug, Deserialize)] +pub enum PatchStep { + NewWorkspace(String), + LoadWorkspace(String), + SaveWorkspace(String), + + CreateSession { + name: String, + session_type: String, + }, + DeleteSession(String), + CopySession(String), + RenameSession { + session: String, + name: String, + }, + + AddItem { + session: String, + name: String, + kind: String, + path: String, + }, + RemoveItem { + session: String, + item: String, + }, + RenameItem { + session: String, + item: String, + name: String, + }, + CopyItem { + from: String, + item: String, + to: String, + }, + MoveItem { + from: String, + item: String, + to: String, + }, + + SelectSession(String), + SelectItem(usize), + NextItem, + PreviousItem, + MoveItemUp, + MoveItemDown, + MoveItemToIndex(usize), + + ViewWorkspace, + ViewSession(String), +} + +pub fn load(path: &Path) -> Result> { + let content = std::fs::read_to_string(path).context("Failed to read patch file")?; + serde_json::from_str(&content).context("Failed to parse patch file") +} + +pub fn run(path: &Path) { + let steps = match load(path) { + Ok(s) => s, + Err(e) => { + eprintln!("patch: {e}"); + return; + } + }; + + let total = steps.len(); + let width = total.to_string().len(); + + println!("patch: '{}' ({total} steps)\n", path.display()); + + let mut ws = Workspace::new("untitled"); + let mut active = ActiveState::new(); + + for (i, step) in steps.into_iter().enumerate() { + let n = i + 1; + print!("[{n:>width$}/{total}] "); + if let Err(e) = execute(step, &mut ws, &mut active) { + println!("[ERR] {e}"); + } + } + + println!("\npatch: done."); +} + +fn execute(step: PatchStep, ws: &mut Workspace, active: &mut ActiveState) -> Result<()> { + match step { + PatchStep::NewWorkspace(name) => { + println!("New Workspace {name:?}"); + *ws = Workspace::new(&name); + active.reset(); + } + PatchStep::LoadWorkspace(path) => { + *ws = Workspace::load(&path)?; + active.reset(); + println!("Load Workspace {path:?} ({} sessions)", ws.sessions.len()); + } + PatchStep::SaveWorkspace(path) => { + ws.save(&path)?; + println!("Save Workspace {path:?}"); + } + + PatchStep::CreateSession { name, session_type } => { + let st = parse_session_type(&session_type)?; + let session = Session::new(&name, st); + let id = session.id; + ws.add_session(session); + active.select_session_by_id(ws, id)?; + println!("Create Session {name:?}"); + } + PatchStep::DeleteSession(name) => { + let id = ws.session_by_name(&name)?.id; + ws.remove_session(id)?; + if active.active_session_id() == Some(id) { + active.clear_session(); + } + println!("Delete Session {name:?}"); + } + PatchStep::CopySession(name) => { + let id = ws.session_by_name(&name)?.id; + let new_id = ws.copy_session(id)?; + let new_name = ws.session(new_id)?.name.clone(); + active.select_session_by_id(ws, new_id)?; + println!("Copy Session {name:?} -> {new_name:?}"); + } + PatchStep::RenameSession { session, name } => { + let id = ws.session_by_name(&session)?.id; + ws.rename_session(id, &name)?; + println!("Rename Session {session:?} -> {name:?}"); + } + + PatchStep::AddItem { + session, + name, + kind, + path, + } => { + let sid = ws.session_by_name(&session)?.id; + let item = Item::new(&name, parse_item_kind(&kind, &path)?); + ws.add_item(sid, item)?; + if active.active_session_id() == Some(sid) { + active.select_last_item(ws); + } + println!("Add Item {name:?} to {session:?}"); + } + PatchStep::RemoveItem { session, item } => { + let sid = ws.session_by_name(&session)?.id; + let iid = ws.item_by_name(sid, &item)?.id; + ws.remove_item(sid, iid)?; + if active.active_session_id() == Some(sid) { + active.adjust_after_remove(ws); + } + println!("Remove Item {item:?} from {session:?}"); + } + PatchStep::RenameItem { + session, + item, + name, + } => { + let sid = ws.session_by_name(&session)?.id; + let iid = ws.item_by_name(sid, &item)?.id; + ws.rename_item(sid, iid, &name)?; + println!("Rename Item {item:?} -> {name:?}"); + } + PatchStep::CopyItem { from, item, to } => { + let from_id = ws.session_by_name(&from)?.id; + let iid = ws.item_by_name(from_id, &item)?.id; + let to_id = ws.session_by_name(&to)?.id; + ws.copy_item(from_id, iid, to_id, usize::MAX)?; + println!("Copy Item {item:?}: {from:?} -> {to:?}"); + } + PatchStep::MoveItem { from, item, to } => { + let from_id = ws.session_by_name(&from)?.id; + let iid = ws.item_by_name(from_id, &item)?.id; + let to_id = ws.session_by_name(&to)?.id; + ws.move_item(from_id, iid, to_id, usize::MAX)?; + if active.active_session_id() == Some(from_id) { + active.adjust_after_remove(ws); + } + println!("Move Item {item:?}: {from:?} -> {to:?}"); + } + + PatchStep::SelectSession(name) => { + active.select_session(ws, &name)?; + println!("Select Session {name:?}"); + print_context(ws, active); + } + PatchStep::SelectItem(index) => { + active.select_item(ws, index)?; + println!("Select Item [{index}]"); + print_context(ws, active); + } + PatchStep::NextItem => { + active.next_item(ws); + println!("Next Item"); + print_context(ws, active); + } + PatchStep::PreviousItem => { + active.previous_item(); + println!("Previous Item"); + print_context(ws, active); + } + PatchStep::MoveItemUp => { + let sid = active.active_session_id().context("No active session")?; + let idx = active.active_item_index().context("No active item")?; + if idx == 0 { + bail!("Already at top") + } + let iid = active.active_item_id(ws).context("No active item")?; + ws.move_item(sid, iid, sid, idx - 1)?; + active.select_item(ws, idx - 1)?; + println!("Move Item Up"); + print_context(ws, active); + } + PatchStep::MoveItemDown => { + let sid = active.active_session_id().context("No active session")?; + let idx = active.active_item_index().context("No active item")?; + let count = active.active_session(ws).map_or(0, |s| s.items.len()); + if idx + 1 >= count { + bail!("Already at bottom") + } + let iid = active.active_item_id(ws).context("No active item")?; + ws.move_item(sid, iid, sid, idx + 1)?; + active.select_item(ws, idx + 1)?; + println!("Move Item Down"); + print_context(ws, active); + } + PatchStep::MoveItemToIndex(target) => { + let sid = active.active_session_id().context("No active session")?; + let iid = active.active_item_id(ws).context("No active item")?; + ws.move_item(sid, iid, sid, target)?; + let count = active.active_session(ws).map_or(0, |s| s.items.len()); + active.select_item(ws, target.min(count.saturating_sub(1)))?; + println!("Move Item To [{target}]"); + print_context(ws, active); + } + + PatchStep::ViewWorkspace => { + println!( + "View Workspace {:?} {} sessions", + ws.name, + ws.sessions.len() + ); + for s in &ws.sessions { + let m = if active.active_session_id() == Some(s.id) { + "=>" + } else { + " " + }; + println!( + " {m} {:?} ({:?}) {} items", + s.name, + s.session_type, + s.items.len() + ); + } + } + PatchStep::ViewSession(name) => { + let session = ws.session_by_name(&name)?; + println!( + "View Session {:?} ({:?}) {} items", + session.name, + session.session_type, + session.items.len() + ); + for (i, item) in session.items.iter().enumerate() { + let m = if active.active_session_id() == Some(session.id) + && active.active_item_index() == Some(i) + { + "=>" + } else { + " " + }; + let kind = match &item.kind { + ItemKind::Portable { .. } => "portable", + ItemKind::Raster { .. } => "raster", + ItemKind::Vector { .. } => "vector", + ItemKind::Document { .. } => "document", + }; + println!(" {m} [{i}] {:?} ({kind})", item.name); + } + } + } + Ok(()) +} + +fn print_context(ws: &Workspace, active: &ActiveState) { + let Some(session) = active.active_session(ws) else { + return; + }; + let item_info = match active.active_item(ws) { + Some(item) => format!( + "[{}] {:?}", + active.active_item_index().unwrap_or(0), + item.name + ), + None => "-".into(), + }; + println!( + " {:?} {} items active: {item_info}", + session.name, + session.items.len() + ); +} + +fn parse_session_type(s: &str) -> Result { + match s.to_lowercase().as_str() { + "browser" => Ok(SessionType::Browser), + "collection" => Ok(SessionType::Collection), + _ => bail!("Unknown session type: {s:?}"), + } +} + +fn parse_item_kind(kind: &str, path: &str) -> Result { + let path = PathBuf::from(path); + match kind.to_lowercase().as_str() { + "portable" | "pdf" => Ok(ItemKind::Portable { path }), + "raster" | "image" => Ok(ItemKind::Raster { path }), + "vector" | "svg" => Ok(ItemKind::Vector { path }), + "document" => Ok(ItemKind::Document { path }), + _ => bail!("Unknown item kind: {kind:?}"), + } +} diff --git a/src/render/converter.rs b/src/render/converter.rs new file mode 100644 index 0000000..a4d02e6 --- /dev/null +++ b/src/render/converter.rs @@ -0,0 +1,4 @@ +// SPDX-License-Identifier: GPL-3.0-or-later +// src/render/converter.rs +// +// Converts raw document data into renderable page output. diff --git a/src/render/mod.rs b/src/render/mod.rs new file mode 100644 index 0000000..c13e7ba --- /dev/null +++ b/src/render/mod.rs @@ -0,0 +1,7 @@ +// SPDX-License-Identifier: GPL-3.0-or-later +// src/render/mod.rs +// +// Rendering subsystem: converts document content into displayable output. + +pub mod converter; +pub mod thumbnail; diff --git a/src/render/thumbnail.rs b/src/render/thumbnail.rs new file mode 100644 index 0000000..294a985 --- /dev/null +++ b/src/render/thumbnail.rs @@ -0,0 +1,4 @@ +// SPDX-License-Identifier: GPL-3.0-or-later +// src/render/thumbnail.rs +// +// Thumbnail cache following the freedesktop.org thumbnail spec. diff --git a/src/ui/app.rs b/src/ui/app.rs deleted file mode 100644 index 2093176..0000000 --- a/src/ui/app.rs +++ /dev/null @@ -1,403 +0,0 @@ -// SPDX-License-Identifier: GPL-3.0-or-later -// src/ui/app/app.rs -// -// COSMIC application wiring and main app struct. - -use super::message::AppMessage; -use super::model::{AppModel, ViewMode}; -use super::update; -use crate::ui::views; - -use std::time::Duration; - -use cosmic::app::{context_drawer, Core}; -use cosmic::cosmic_config::{self, CosmicConfigEntry}; -use cosmic::iced::keyboard::{self, key::Named, Key, Modifiers}; -use cosmic::iced::time; -use cosmic::iced::window; -use cosmic::iced::Subscription; -use cosmic::widget::nav_bar; -use cosmic::{Action, Element, Task}; - -use crate::application::DocumentManager; -use crate::config::AppConfig; -use crate::Args; - -/// Flags passed from `main` into the application. -#[derive(Debug, Clone)] -pub enum Flags { - Args(Args), -} - -/// Context page displayed in right drawer. -#[derive(Copy, Clone, Debug, Default, Eq, PartialEq)] -pub enum ContextPage { - #[default] - Properties, -} - -/// Main application type. -pub struct NoctuaApp { - core: Core, - pub model: AppModel, - nav: nav_bar::Model, - context_page: ContextPage, - pub config: AppConfig, - config_handler: Option, - pub document_manager: DocumentManager, -} - -impl cosmic::Application for NoctuaApp { - type Executor = cosmic::SingleThreadExecutor; - type Flags = Flags; - type Message = AppMessage; - - const APP_ID: &'static str = "org.codeberg.wfx.Noctua"; - - fn core(&self) -> &Core { - &self.core - } - - fn core_mut(&mut self) -> &mut Core { - &mut self.core - } - - fn init(mut core: Core, flags: Self::Flags) -> (Self, Task>) { - // Load persisted config. - let (config, config_handler) = - match cosmic_config::Config::new(Self::APP_ID, AppConfig::VERSION) { - Ok(handler) => { - let config = AppConfig::get_entry(&handler).unwrap_or_default(); - (config, Some(handler)) - } - Err(_) => (AppConfig::default(), None), - }; - - let Flags::Args(args) = flags; - - // Determine initial path: CLI argument takes priority. - // Fall back to configured default directory only if it exists. - let initial_path = args.file.or_else(|| { - config - .default_image_dir - .as_ref() - .filter(|p| p.exists()) - .cloned() - }); - - // Initialize document manager - let mut document_manager = DocumentManager::new(); - - // Initialize model - let mut model = AppModel::new(config.clone()); - - // Load initial document if provided - if let Some(path) = initial_path { - if let Err(e) = document_manager.open_document(&path) { - log::error!("Failed to open initial path {}: {}", path.display(), e); - } else { - // Set initial view mode to Fit - model.viewport.fit_mode = ViewMode::Fit; - model.viewport.scale = 1.0; - model.reset_pan(); - - // Cache initial render so image is displayed immediately - if let Some(doc) = document_manager.current_document_mut() { - use crate::domain::document::core::document::Renderable; - match doc.render(model.viewport.scale as f64) { - Ok(output) => { - model.viewport.cached_image_handle = Some(output.handle); - } - Err(e) => { - log::error!("Failed to render initial document: {}", e); - } - } - } - } - } - - // Initialize nav bar model (required for COSMIC to show toggle icon). - let nav = nav_bar::Model::default(); - - // Apply persisted panel states. - core.window.show_context = config.context_drawer_visible; - - // Auto-open nav bar for multi-page documents - let should_show_nav = if let Some(doc) = document_manager.current_document() { - doc.is_multi_page() - } else { - false - }; - - if should_show_nav { - core.nav_bar_set_toggled(true); - model.panels.left = Some(crate::ui::model::LeftPanel::Thumbnails); - } else { - core.nav_bar_set_toggled(config.nav_bar_visible); - } - - // Start thumbnail generation for initial document if applicable. - let init_task = start_thumbnail_generation(&model); - - ( - Self { - core, - model, - nav, - context_page: ContextPage::default(), - config, - config_handler, - document_manager, - }, - init_task, - ) - } - - fn on_close_requested(&self, _id: window::Id) -> Option { - None - } - - fn update(&mut self, message: Self::Message) -> Task> { - match &message { - AppMessage::ToggleNavBar => { - use crate::ui::model::LeftPanel; - - self.core.nav_bar_toggle(); - let is_visible = self.core.nav_bar_active(); - self.config.nav_bar_visible = is_visible; - self.save_config(); - - if is_visible { - // Opening nav bar - show thumbnails for multi-page docs - if let Some(doc) = self.document_manager.current_document() - && doc.is_multi_page() - { - self.model.panels.left = Some(LeftPanel::Thumbnails); - } - } else { - // Closing nav bar - hide left panel - self.model.panels.left = None; - } - return Task::none(); - } - - AppMessage::OpenFormatPanel => { - // Format panel is now part of Transform mode - // Switch to Transform mode which shows format tools in right panel - self.model.mode = crate::ui::model::AppMode::Transform { - paper_format: None, - orientation: crate::ui::model::Orientation::default(), - }; - - return Task::none(); - } - - AppMessage::ToggleContextPage(page) => { - if self.context_page == *page { - self.core.window.show_context = !self.core.window.show_context; - } else { - self.context_page = *page; - self.core.window.show_context = true; - } - self.config.context_drawer_visible = self.core.window.show_context; - self.save_config(); - return Task::none(); - } - - AppMessage::OpenPath(_) | AppMessage::NextDocument | AppMessage::PrevDocument => { - let result = update::update(self, &message); - let thumb_task = start_thumbnail_generation_task(&self.model); - return match result { - update::UpdateResult::None => thumb_task, - update::UpdateResult::Task(task) => Task::batch([task, thumb_task]), - }; - } - - _ => {} - } - - match update::update(self, &message) { - update::UpdateResult::None => Task::none(), - update::UpdateResult::Task(task) => task, - } - } - - fn header_start(&self) -> Vec> { - views::header::start(&self.model, &self.document_manager) - } - - fn header_end(&self) -> Vec> { - views::header::end(&self.model, &self.document_manager) - } - - fn view(&self) -> Element<'_, Self::Message> { - views::view(&self.model, &self.document_manager, &self.config) - } - - fn context_drawer(&self) -> Option> { - if !self.core.window.show_context { - return None; - } - Some(context_drawer::context_drawer( - views::panels::view(&self.model, &self.document_manager), - AppMessage::ToggleContextPage(ContextPage::Properties), - )) - } - - fn nav_model(&self) -> Option<&nav_bar::Model> { - Some(&self.nav) - } - - fn nav_bar(&self) -> Option>> { - if !self.core.nav_bar_active() { - return None; - } - views::nav_bar(&self.model, &self.document_manager) - } - - fn footer(&self) -> Option> { - Some(views::footer::view(&self.model, &self.document_manager)) - } - - fn subscription(&self) -> Subscription { - Subscription::batch([ - keyboard::on_key_press(handle_key_press), - thumbnail_refresh_subscription(self), - ]) - } -} - -impl NoctuaApp { - /// Save current config to disk. - fn save_config(&self) { - if let Some(ref handler) = self.config_handler { - let _ = self.config.write_entry(handler); - } - } - - /// Update nav bar visibility based on current document type. - pub fn update_nav_bar_for_document(&mut self) { - use crate::ui::model::LeftPanel; - - if let Some(doc) = self.document_manager.current_document() { - if doc.is_multi_page() { - // Multi-page document: open nav bar and show thumbnails - self.core.nav_bar_set_toggled(true); - self.model.panels.left = Some(LeftPanel::Thumbnails); - } else { - // Single-page document: close nav bar - self.core.nav_bar_set_toggled(false); - self.model.panels.left = None; - } - } - } -} - -/// Map raw key presses + modifiers into high-level application messages. -fn handle_key_press(key: Key, modifiers: Modifiers) -> Option { - use AppMessage::{ - PanLeft, PanRight, PanUp, PanDown, OpenFormatPanel, NextDocument, PrevDocument, - FlipHorizontal, FlipVertical, RotateCCW, RotateCW, ZoomIn, ZoomOut, ZoomReset, ZoomFit, - ToggleCropMode, ToggleScaleMode, PanReset, ToggleContextPage, ToggleNavBar, SetAsWallpaper, - }; - - // Handle Ctrl + arrow keys for panning. - if modifiers.control() && !modifiers.shift() && !modifiers.alt() && !modifiers.logo() { - return match key.as_ref() { - Key::Named(Named::ArrowLeft) => Some(PanLeft), - Key::Named(Named::ArrowRight) => Some(PanRight), - Key::Named(Named::ArrowUp) => Some(PanUp), - Key::Named(Named::ArrowDown) => Some(PanDown), - Key::Character(ch) if ch.eq_ignore_ascii_case("f") => Some(OpenFormatPanel), - _ => None, - }; - } - - // Ignore key presses when command-style modifiers are pressed. - if modifiers.command() || modifiers.alt() || modifiers.logo() || modifiers.control() { - return None; - } - - match key.as_ref() { - // Navigation with arrow keys (no modifiers). - Key::Named(Named::ArrowRight) => Some(NextDocument), - Key::Named(Named::ArrowLeft) => Some(PrevDocument), - - // Transformations. - Key::Character(ch) if ch.eq_ignore_ascii_case("h") => Some(FlipHorizontal), - Key::Character(ch) if ch.eq_ignore_ascii_case("v") => Some(FlipVertical), - Key::Character(ch) if ch.eq_ignore_ascii_case("r") => { - if modifiers.shift() { - Some(RotateCCW) - } else { - Some(RotateCW) - } - } - - // Zoom. - Key::Character("+" | "=") => Some(ZoomIn), - Key::Character("-") => Some(ZoomOut), - Key::Character("1") => Some(ZoomReset), - Key::Character(ch) if ch.eq_ignore_ascii_case("f") => Some(ZoomFit), - - // Tool modes. - Key::Character(ch) if ch.eq_ignore_ascii_case("c") => Some(ToggleCropMode), - Key::Character(ch) if ch.eq_ignore_ascii_case("s") => Some(ToggleScaleMode), - - // Crop mode actions (Enter/Escape handled via key press, validated in update). - Key::Named(Named::Enter) => Some(AppMessage::ApplyCrop), - Key::Named(Named::Escape) => Some(AppMessage::CancelCrop), - - // Reset pan. - Key::Character("0") => Some(PanReset), - - // Toggle panels. - Key::Character(ch) if ch.eq_ignore_ascii_case("i") => { - Some(ToggleContextPage(ContextPage::Properties)) - } - Key::Character(ch) if ch.eq_ignore_ascii_case("n") => Some(ToggleNavBar), - - // Wallpaper. - Key::Character(ch) if ch.eq_ignore_ascii_case("w") => Some(SetAsWallpaper), - - _ => None, - } -} - -// ============================================================================= -// Thumbnail Helpers -// ============================================================================= - -fn start_thumbnail_generation(model: &AppModel) -> Task> { - start_thumbnail_generation_task(model) -} - -fn start_thumbnail_generation_task(_model: &AppModel) -> Task> { - // TODO: Re-enable when document is synced from DocumentManager - // if let Some(doc) = &model.document { - // let page_count = doc.page_count(); - // if page_count > 0 && !doc.thumbnails_ready() { - // return Task::batch([ - // Task::done(Action::App(AppMessage::GenerateThumbnailPage(0))), - // Task::done(Action::App(AppMessage::RefreshView)), - // ]); - // } - // } - Task::none() -} - -fn thumbnail_refresh_subscription(_app: &NoctuaApp) -> Subscription { - // TODO: Re-enable when document is synced from DocumentManager - let needs_refresh = false; - // let needs_refresh = app - // .model - // .document - // .as_ref() - // .is_some_and(|doc| doc.is_multi_page() && !doc.thumbnails_ready()); - - if needs_refresh { - time::every(Duration::from_millis(100)).map(|_| AppMessage::RefreshView) - } else { - Subscription::none() - } -} diff --git a/src/ui/components/mod.rs b/src/ui/components/mod.rs deleted file mode 100644 index debd9c5..0000000 --- a/src/ui/components/mod.rs +++ /dev/null @@ -1,6 +0,0 @@ -// SPDX-License-Identifier: GPL-3.0-or-later -// src/ui/components/mod.rs -// -// UI components: reusable widgets and controls. - -// Crop functionality moved to ui/widgets diff --git a/src/ui/message.rs b/src/ui/message.rs deleted file mode 100644 index 18826e3..0000000 --- a/src/ui/message.rs +++ /dev/null @@ -1,102 +0,0 @@ -// SPDX-License-Identifier: GPL-3.0-or-later -// src/app/message.rs -// -// Application messages: events, user actions, and internal signals. - -use std::path::PathBuf; - -use crate::ui::widgets::DragHandle; - -#[derive(Debug, Clone)] -pub enum AppMessage { - // File / navigation. - #[allow(dead_code)] - OpenPath(PathBuf), - NextDocument, - PrevDocument, - GotoPage(usize), - GenerateThumbnailPage(usize), - - // Transformations. - RotateCW, - RotateCCW, - FlipHorizontal, - FlipVertical, - - // View / zoom. - ZoomIn, - ZoomOut, - ZoomReset, - ZoomFit, - ViewerStateChanged { - scale: f32, - offset_x: f32, - offset_y: f32, - canvas_size: cosmic::iced::Size, - image_size: cosmic::iced::Size, - }, - - // Pan control. - PanLeft, - PanRight, - PanUp, - PanDown, - PanReset, - - // Tool modes. - ToggleCropMode, - ToggleScaleMode, - - // Crop operations. - StartCrop, - CancelCrop, - ApplyCrop, - CropDragStart { - x: f32, - y: f32, - handle: DragHandle, - }, - CropDragMove { - x: f32, - y: f32, - max_x: f32, - max_y: f32, - }, - - CropDragEnd, - - // Panels. - ToggleContextPage(crate::ui::app::ContextPage), - ToggleNavBar, - OpenFormatPanel, - - // Menu. - ToggleMainMenu, - - // Format operations. - SetPaperFormat(super::model::PaperFormat), - SetOrientation(super::model::Orientation), - - // Metadata. - #[allow(dead_code)] - RefreshMetadata, - - // Save operations. - SaveAs, - - // Wallpaper. - SetAsWallpaper, - - // Errors. - #[allow(dead_code)] - ShowError(String), - #[allow(dead_code)] - ClearError, - - // UI refresh. - RefreshView, - - // Fallback. - #[allow(dead_code)] - NoOp, -} diff --git a/src/ui/mod.rs b/src/ui/mod.rs deleted file mode 100644 index 96a0990..0000000 --- a/src/ui/mod.rs +++ /dev/null @@ -1,17 +0,0 @@ -// SPDX-License-Identifier: GPL-3.0-or-later -// src/ui/mod.rs -// -// UI layer: COSMIC application, views, and widgets. - -pub mod app; -pub mod message; -pub mod model; -pub mod update; -pub mod components; -pub mod views; -pub mod widgets; - -// Re-export main types -pub use app::NoctuaApp; -pub use message::AppMessage; -pub use model::AppModel; diff --git a/src/ui/model.rs b/src/ui/model.rs deleted file mode 100644 index ef22573..0000000 --- a/src/ui/model.rs +++ /dev/null @@ -1,280 +0,0 @@ -// SPDX-License-Identifier: GPL-3.0-or-later -// src/ui/model.rs -// -// UI state (view, tools, panels). -// -// AppModel contains ONLY UI-specific state. -// Document state lives in DocumentManager (application layer). - -use cosmic::iced::Size; - -use crate::ui::widgets::CropSelection; -use crate::config::AppConfig; - -// ============================================================================= -// View Mode -// ============================================================================= - -#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)] -pub enum ViewMode { - #[default] - Fit, - ActualSize, - Custom, -} - -// ============================================================================= -// Paper Format (for export/transform) -// ============================================================================= - -#[derive(Debug, Clone, Copy, PartialEq, Eq)] -pub enum PaperFormat { - UsLetter, - IsoA0, - IsoA1, - IsoA2, - IsoA3, - IsoA4, - IsoA5, - IsoA6, -} - -impl PaperFormat { - /// Returns (width, height) in millimeters - pub fn dimensions_mm(self) -> (u32, u32) { - match self { - Self::UsLetter => (216, 279), // 8.5 x 11 inches - Self::IsoA0 => (841, 1189), - Self::IsoA1 => (594, 841), - Self::IsoA2 => (420, 594), - Self::IsoA3 => (297, 420), - Self::IsoA4 => (210, 297), - Self::IsoA5 => (148, 210), - Self::IsoA6 => (105, 148), - } - } - - /// Returns display name - pub fn display_name(self) -> &'static str { - match self { - Self::UsLetter => "US Letter", - Self::IsoA0 => "A0 (841 × 1189 mm)", - Self::IsoA1 => "A1", - Self::IsoA2 => "A2", - Self::IsoA3 => "A3", - Self::IsoA4 => "A4", - Self::IsoA5 => "A5 (148 × 210 mm)", - Self::IsoA6 => "A6", - } - } -} - -#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)] -pub enum Orientation { - Horizontal, - #[default] - Vertical, -} - -// ============================================================================= -// Application Mode (combines tool + panel state) -// ============================================================================= - -/// Application mode - unified tool and panel state. -/// -/// Each mode determines: -/// - Active tool behavior -/// - Right panel content -/// - Available shortcuts -#[derive(Debug, Clone)] -#[allow(dead_code)] -pub enum AppMode { - /// Normal viewing mode - no active tool - View, - - /// Crop mode with selection - Crop { selection: CropSelection }, - - /// Transform/export mode - Transform { - paper_format: Option, - orientation: Orientation, - }, - - /// Fullscreen mode (all panels hidden) - Fullscreen, -} - -impl Default for AppMode { - fn default() -> Self { - Self::View - } -} - -impl AppMode { - /// Get the right panel that should be shown for this mode - pub fn right_panel(&self) -> Option { - match self { - Self::View => Some(RightPanel::Properties), - Self::Crop { .. } => Some(RightPanel::CropTools), - Self::Transform { .. } => Some(RightPanel::TransformTools), - Self::Fullscreen => None, - } - } - - /// Check if mode is an active tool (not View/Fullscreen) - pub fn is_tool_active(&self) -> bool { - matches!(self, Self::Crop { .. } | Self::Transform { .. }) - } -} - -// ============================================================================= -// Viewport (zoom, pan, canvas) -// ============================================================================= - -/// Viewport state - zoom, pan, canvas dimensions. -#[derive(Debug, Clone)] -pub struct Viewport { - /// Current scale factor - pub scale: f32, - - /// Pan offset X - pub pan_x: f32, - - /// Pan offset Y - pub pan_y: f32, - - /// Canvas size (container) - pub canvas_size: Size, - - /// Image size (after scaling) - pub image_size: Size, - - /// Fit mode - pub fit_mode: ViewMode, - - /// Scroll container ID - pub scroll_id: cosmic::widget::Id, - - /// Cached image handle for rendering (updated when document or scale changes) - pub cached_image_handle: Option, -} - -impl Default for Viewport { - fn default() -> Self { - Self { - scale: 1.0, - pan_x: 0.0, - pan_y: 0.0, - canvas_size: Size::ZERO, - image_size: Size::ZERO, - fit_mode: ViewMode::Fit, - scroll_id: cosmic::widget::Id::new("canvas-scroll"), - cached_image_handle: None, - } - } -} - -impl Viewport { - /// Reset pan to center - pub fn reset_pan(&mut self) { - self.pan_x = 0.0; - self.pan_y = 0.0; - } -} - -// ============================================================================= -// Panel State -// ============================================================================= - -/// Panel visibility state. -#[derive(Debug, Clone, Default)] -pub struct PanelState { - /// Left panel (thumbnails for multi-page) - pub left: Option, - - /// Right panel (context-dependent tools/properties) - pub right: Option, -} - -/// Left panel types -#[derive(Debug, Clone, Copy, PartialEq, Eq)] -pub enum LeftPanel { - /// Thumbnail navigation for multi-page documents - Thumbnails, -} - -/// Right panel types -#[derive(Debug, Clone, Copy, PartialEq, Eq)] -#[allow(dead_code)] -pub enum RightPanel { - /// Document properties and metadata - Properties, - - /// Crop mode tools - CropTools, - - /// Transform/export tools - TransformTools, -} - -// ============================================================================= -// AppModel (UI State Only) -// ============================================================================= - -/// UI state for the application. -/// -/// Contains ONLY UI-specific state: -/// - Current mode (view/tool) -/// - Viewport (zoom/pan) -/// - Panel visibility -/// - Transient UI state (errors, menu) -/// -/// Document state (current file, metadata, etc.) lives in DocumentManager! -pub struct AppModel { - /// Current application mode - pub mode: AppMode, - - /// Viewport state - pub viewport: Viewport, - - /// Panel visibility - pub panels: PanelState, - - /// Error message to display - pub error: Option, - - /// Is main menu open? - pub menu_open: bool, - - /// Tick counter for animations - pub tick: u64, -} - -impl AppModel { - pub fn new(_config: AppConfig) -> Self { - Self { - mode: AppMode::default(), - viewport: Viewport::default(), - panels: PanelState::default(), - error: None, - menu_open: false, - tick: 0, - } - } - - /// Set error message - pub fn set_error>(&mut self, msg: S) { - self.error = Some(msg.into()); - } - - /// Clear error message - pub fn clear_error(&mut self) { - self.error = None; - } - - /// Reset viewport pan to center - pub fn reset_pan(&mut self) { - self.viewport.reset_pan(); - } -} diff --git a/src/ui/update.rs b/src/ui/update.rs deleted file mode 100644 index a511072..0000000 --- a/src/ui/update.rs +++ /dev/null @@ -1,404 +0,0 @@ -// SPDX-License-Identifier: GPL-3.0-or-later -// src/ui/update.rs -// -// Application update loop: applies messages to the global model state. - -use cosmic::{Action, Task}; - -use super::NoctuaApp; -use super::message::AppMessage; -use super::model::{AppMode, ViewMode}; -use crate::application::commands::transform_document::{TransformDocumentCommand, TransformOperation}; -use crate::application::commands::crop_document::CropDocumentCommand; -use crate::domain::document::core::document::Renderable; -use crate::ui::widgets::{CropSelection, DragHandle}; - -// ============================================================================= -// Update Result -// ============================================================================= - -#[allow(dead_code)] -pub enum UpdateResult { - None, - Task(Task>), -} - -// ============================================================================= -// Main Update Function -// ============================================================================= - -pub fn update(app: &mut NoctuaApp, msg: &AppMessage) -> UpdateResult { - match msg { - // ---- File / navigation ---------------------------------------------------- - AppMessage::OpenPath(path) => { - if let Err(e) = app.document_manager.open_document(path) { - app.model.set_error(format!("Failed to open document: {e}")); - } else { - app.model.reset_pan(); - app.model.viewport.fit_mode = ViewMode::Fit; - app.model.viewport.scale = 1.0; - cache_render(&mut app.model, &mut app.document_manager); - - // Auto-toggle nav bar for multi-page documents - app.update_nav_bar_for_document(); - } - } - - AppMessage::NextDocument => { - // Ignore navigation in Crop mode - if !matches!(app.model.mode, AppMode::Crop { .. }) - && let Some(_path) = app.document_manager.next_document() - { - // Reset zoom when navigating to new document - app.model.viewport.scale = 1.0; - app.model.viewport.fit_mode = ViewMode::Fit; - app.model.reset_pan(); - cache_render(&mut app.model, &mut app.document_manager); - - // Auto-toggle nav bar for multi-page documents - app.update_nav_bar_for_document(); - } - } - - AppMessage::PrevDocument => { - // Ignore navigation in Crop mode - if !matches!(app.model.mode, AppMode::Crop { .. }) - && let Some(_path) = app.document_manager.previous_document() - { - // Reset zoom when navigating to new document - app.model.viewport.scale = 1.0; - app.model.viewport.fit_mode = ViewMode::Fit; - app.model.reset_pan(); - cache_render(&mut app.model, &mut app.document_manager); - - // Auto-toggle nav bar for multi-page documents - app.update_nav_bar_for_document(); - } - } - - AppMessage::GotoPage(page) => { - if let Some(doc) = app.document_manager.current_document_mut() { - if let Err(e) = doc.go_to_page(*page) { - log::error!("Failed to navigate to page {page}: {e}"); - } else { - cache_render(&mut app.model, &mut app.document_manager); - } - } - } - - // ---- Thumbnail generation ------------------------------------------------- - AppMessage::GenerateThumbnailPage(_page) => { - // TODO: Thumbnail generation via DocumentManager - // Currently handled by DocumentManager.open_document() - } - - AppMessage::RefreshView => { - app.model.tick += 1; - } - - // ---- View / zoom --------------------------------------------------------- - AppMessage::ZoomIn => { - app.model.viewport.scale = (app.model.viewport.scale * 1.2).min(10.0); - app.model.viewport.fit_mode = ViewMode::Custom; - } - - AppMessage::ZoomOut => { - app.model.viewport.scale = (app.model.viewport.scale / 1.2).max(0.1); - app.model.viewport.fit_mode = ViewMode::Custom; - } - - AppMessage::ZoomReset => { - app.model.viewport.scale = 1.0; - app.model.viewport.fit_mode = ViewMode::ActualSize; - app.model.reset_pan(); - } - - AppMessage::ZoomFit => { - app.model.viewport.fit_mode = ViewMode::Fit; - app.model.reset_pan(); - } - - AppMessage::ViewerStateChanged { - scale, - offset_x, - offset_y, - canvas_size, - image_size, - } => { - // Detect scale changes (zoom vs just pan) - let old_scale = app.model.viewport.scale; - - // Update model from viewer state - app.model.viewport.scale = *scale; - app.model.viewport.pan_x = *offset_x; - app.model.viewport.pan_y = *offset_y; - app.model.viewport.canvas_size = *canvas_size; - app.model.viewport.image_size = *image_size; - - // If scale changed, user zoomed -> switch to Custom mode and re-render - // (Fit mode is only maintained when explicitly set via ZoomFit button) - if (old_scale - *scale).abs() > 0.001 { - app.model.viewport.fit_mode = ViewMode::Custom; - cache_render(&mut app.model, &mut app.document_manager); - } - } - - // ---- Pan control --------------------------------------------------------- - AppMessage::PanLeft => { - app.model.viewport.pan_x -= 50.0; - } - AppMessage::PanRight => { - app.model.viewport.pan_x += 50.0; - } - AppMessage::PanUp => { - app.model.viewport.pan_y -= 50.0; - } - AppMessage::PanDown => { - app.model.viewport.pan_y += 50.0; - } - AppMessage::PanReset => { - app.model.reset_pan(); - } - - // ---- Tool modes ---------------------------------------------------------- - AppMessage::ToggleCropMode => { - app.model.mode = match &app.model.mode { - AppMode::Crop { .. } => AppMode::View, - _ => AppMode::Crop { - selection: CropSelection::default(), - }, - }; - } - - AppMessage::ToggleScaleMode => { - // Scale mode -> Transform mode - app.model.mode = match &app.model.mode { - AppMode::Transform { .. } => AppMode::View, - _ => AppMode::Transform { - paper_format: None, - orientation: Default::default(), - }, - }; - } - - // ---- Crop operations ----------------------------------------------------- - AppMessage::StartCrop => { - if app.document_manager.current_document().is_some() { - app.model.mode = AppMode::Crop { - selection: CropSelection::default(), - }; - } - } - - AppMessage::CancelCrop => { - // Only cancel if actually in Crop mode - if matches!(app.model.mode, AppMode::Crop { .. }) { - app.model.mode = AppMode::View; - } - } - - AppMessage::ApplyCrop => { - if let AppMode::Crop { selection } = &app.model.mode { - // Get crop selection region - if let Some(crop_region) = selection.to_crop_region() { - // Create crop command from canvas selection - let pan_offset = cosmic::iced::Vector::new( - app.model.viewport.pan_x, - app.model.viewport.pan_y, - ); - - match CropDocumentCommand::from_canvas_selection( - &crop_region, - app.model.viewport.canvas_size, - app.model.viewport.image_size, - app.model.viewport.scale, - pan_offset, - ) { - Ok(cmd) => { - // Execute crop command - if let Err(e) = cmd.execute(&mut app.document_manager) { - app.model.set_error(format!("Crop failed: {e}")); - } else { - // Success - exit crop mode - app.model.mode = AppMode::View; - // Reset view to fit the cropped image - app.model.viewport.scale = 1.0; - app.model.viewport.fit_mode = ViewMode::Fit; - app.model.reset_pan(); - cache_render(&mut app.model, &mut app.document_manager); - } - } - Err(e) => { - app.model.set_error(format!("Invalid crop region: {e}")); - } - } - } else { - app.model.set_error("No crop region selected".to_string()); - } - } - } - - AppMessage::CropDragStart { x, y, handle } => { - if let AppMode::Crop { selection } = &mut app.model.mode { - if *handle == DragHandle::None { - selection.start_new_selection(*x, *y); - } else { - selection.start_handle_drag(*handle, *x, *y); - } - } - } - - AppMessage::CropDragMove { x, y, max_x, max_y } => { - if let AppMode::Crop { selection } = &mut app.model.mode { - selection.update_drag(*x, *y, *max_x, *max_y); - } - } - - AppMessage::CropDragEnd => { - if let AppMode::Crop { selection } = &mut app.model.mode { - selection.end_drag(); - } - } - - // ---- Save operations ----------------------------------------------------- - AppMessage::SaveAs => { - save_as(&mut app.model); - } - - // ---- Document transformations -------------------------------------------- - AppMessage::FlipHorizontal => { - // Ignore transformations in Crop mode (would invalidate selection) - if !matches!(app.model.mode, AppMode::Crop { .. }) { - let cmd = TransformDocumentCommand::new(TransformOperation::FlipHorizontal); - if let Err(e) = cmd.execute(&mut app.document_manager) { - app.model.set_error(format!("Flip horizontal failed: {e}")); - } else { - cache_render(&mut app.model, &mut app.document_manager); - } - } - } - - AppMessage::FlipVertical => { - // Ignore transformations in Crop mode (would invalidate selection) - if !matches!(app.model.mode, AppMode::Crop { .. }) { - let cmd = TransformDocumentCommand::new(TransformOperation::FlipVertical); - if let Err(e) = cmd.execute(&mut app.document_manager) { - app.model.set_error(format!("Flip vertical failed: {e}")); - } else { - cache_render(&mut app.model, &mut app.document_manager); - } - } - } - - AppMessage::RotateCW => { - // Ignore transformations in Crop mode (would invalidate selection) - if !matches!(app.model.mode, AppMode::Crop { .. }) { - let cmd = TransformDocumentCommand::new(TransformOperation::RotateCw); - if let Err(e) = cmd.execute(&mut app.document_manager) { - app.model.set_error(format!("Rotate clockwise failed: {e}")); - } else { - cache_render(&mut app.model, &mut app.document_manager); - } - } - } - - AppMessage::RotateCCW => { - // Ignore transformations in Crop mode (would invalidate selection) - if !matches!(app.model.mode, AppMode::Crop { .. }) { - let cmd = TransformDocumentCommand::new(TransformOperation::RotateCcw); - if let Err(e) = cmd.execute(&mut app.document_manager) { - app.model.set_error(format!("Rotate CCW failed: {e}")); - } else { - cache_render(&mut app.model, &mut app.document_manager); - } - } - } - - // ---- Metadata ------------------------------------------------------------ - AppMessage::RefreshMetadata => { - // Metadata is managed by DocumentManager - // Nothing to do here - views access it directly - } - - // ---- Format operations --------------------------------------------------- - AppMessage::SetPaperFormat(format) => { - if let AppMode::Transform { paper_format, .. } = &mut app.model.mode { - *paper_format = Some(*format); - } - } - - AppMessage::SetOrientation(orientation) => { - if let AppMode::Transform { - orientation: ori, .. - } = &mut app.model.mode - { - *ori = *orientation; - } - } - - // ---- Menu ---------------------------------------------------------------- - AppMessage::ToggleMainMenu => { - app.model.menu_open = !app.model.menu_open; - } - - // ---- Wallpaper ----------------------------------------------------------- - AppMessage::SetAsWallpaper => { - if let Some(path) = app.document_manager.current_path() { - log::info!("Setting wallpaper to: {}", path.display()); - crate::infrastructure::system::set_as_wallpaper(path); - } else { - app.model.set_error("No image loaded".to_string()); - } - } - - // ---- Error handling ------------------------------------------------------ - AppMessage::ShowError(msg) => { - app.model.set_error(msg.clone()); - } - - AppMessage::ClearError => { - app.model.clear_error(); - } - - // ---- Handled elsewhere --------------------------------------------------- - AppMessage::ToggleContextPage(_) - | AppMessage::ToggleNavBar - | AppMessage::OpenFormatPanel => { - // These are handled in app.rs - } - - AppMessage::NoOp => {} - } - - UpdateResult::None -} - -// ============================================================================= -// Helper Functions -// ============================================================================= - -/// Cache rendered image handle in viewport for view performance. -fn cache_render( - model: &mut super::model::AppModel, - manager: &mut crate::application::DocumentManager, -) { - if let Some(doc) = manager.current_document_mut() { - match doc.render(model.viewport.scale as f64) { - Ok(output) => { - model.viewport.cached_image_handle = Some(output.handle); - } - Err(e) => { - log::error!("Failed to cache render: {e}"); - model.viewport.cached_image_handle = None; - } - } - } else { - model.viewport.cached_image_handle = None; - } -} - -fn save_as(model: &mut super::model::AppModel) { - // TODO: Implement file dialog for save path - // For now, show error that this needs UI integration - model.set_error("Save As: File dialog not yet implemented".to_string()); -} diff --git a/src/ui/views/canvas.rs b/src/ui/views/canvas.rs deleted file mode 100644 index 68d4729..0000000 --- a/src/ui/views/canvas.rs +++ /dev/null @@ -1,79 +0,0 @@ -// SPDX-License-Identifier: GPL-3.0-or-later -// src/ui/views/canvas.rs -// -// Render the center canvas area with the current document. - -use cosmic::iced::widget::image::FilterMethod; -use cosmic::iced::{ContentFit, Length}; -use cosmic::iced_widget::stack; -use cosmic::widget::{container, text}; -use cosmic::Element; - -use crate::ui::widgets::{crop_overlay, Viewer}; -use crate::ui::model::{AppMode, ViewMode}; -use crate::ui::{AppMessage, AppModel}; -use crate::application::DocumentManager; -use crate::config::AppConfig; -use crate::fl; - -/// Render the center canvas area with the current document. -pub fn view<'a>( - model: &'a AppModel, - _manager: &'a DocumentManager, - config: &'a AppConfig, -) -> Element<'a, AppMessage> { - // Use cached image handle from viewport - if let Some(handle) = &model.viewport.cached_image_handle { - // Determine content fit mode - let content_fit = match model.viewport.fit_mode { - ViewMode::Fit => ContentFit::Contain, - ViewMode::ActualSize | ViewMode::Custom => ContentFit::None, - }; - - // Check if we're in crop mode (to disable pan) - let disable_pan = matches!(model.mode, AppMode::Crop { .. }); - - // Create image viewer - let img_viewer = Viewer::new(handle.clone()) - .with_state( - model.viewport.scale, - model.viewport.pan_x, - model.viewport.pan_y, - ) - .on_state_change(|scale, offset_x, offset_y, canvas_size, image_size| { - AppMessage::ViewerStateChanged { - scale, - offset_x, - offset_y, - canvas_size, - image_size, - } - }) - .width(Length::Fill) - .height(Length::Fill) - .content_fit(content_fit) - .filter_method(FilterMethod::Linear) - .min_scale(config.min_scale) - .max_scale(config.max_scale) - .scale_step(config.scale_step - 1.0) - .disable_pan(disable_pan); - - // Overlay crop UI when in crop mode - if let AppMode::Crop { selection } = &model.mode { - let overlay = crop_overlay(selection, config.crop_show_grid); - stack![img_viewer, overlay].into() - } else { - container(img_viewer) - .width(Length::Fill) - .height(Length::Fill) - .into() - } - } else { - // No document loaded - container(text(fl!("no-document"))) - .width(Length::Fill) - .height(Length::Fill) - .center(Length::Fill) - .into() - } -} diff --git a/src/ui/views/footer.rs b/src/ui/views/footer.rs deleted file mode 100644 index b9770ea..0000000 --- a/src/ui/views/footer.rs +++ /dev/null @@ -1,87 +0,0 @@ -// SPDX-License-Identifier: GPL-3.0-or-later -// src/ui/views/footer.rs -// -// Footer bar with zoom controls and document info. - -use cosmic::iced::Alignment; -use cosmic::widget::{button, icon, row, text}; -use cosmic::Element; - -use crate::ui::model::{AppModel, ViewMode}; -use crate::ui::AppMessage; -use crate::application::DocumentManager; -use crate::domain::document::core::document::Renderable; -use crate::fl; - -/// Build the footer element with zoom controls and document info. -pub fn view<'a>(model: &'a AppModel, manager: &'a DocumentManager) -> Element<'a, AppMessage> { - // Zoom level display - let zoom_text = if model.viewport.fit_mode == ViewMode::Fit { - fl!("status-zoom-fit") - } else { - let percent = (model.viewport.scale * 100.0).round() as i32; - fl!("status-zoom-percent", percent: percent) - }; - - // Document dimensions (from DocumentManager) - let doc_info = if let Some(doc) = manager.current_document() { - let info = doc.info(); - fl!("status-doc-dimensions", width: info.width, height: info.height) - } else { - String::new() - }; - - // Navigation position (from DocumentManager) - let folder_count = manager.folder_entries().len(); - let nav_info = if folder_count == 0 { - String::new() - } else { - let current = manager.current_index().map_or(0, |i| i + 1); - let total = folder_count; - fl!("status-nav-position", current: current, total: total) - }; - - row() - .spacing(8) - .align_y(Alignment::Center) - .padding([4, 12]) - // Zoom out button - .push( - button::icon(icon::from_name("zoom-out-symbolic")) - .on_press(AppMessage::ZoomOut) - .padding(4), - ) - // Zoom level text - .push(text(zoom_text)) - // Zoom in button - .push( - button::icon(icon::from_name("zoom-in-symbolic")) - .on_press(AppMessage::ZoomIn) - .padding(4), - ) - // Zoom reset button - .push( - button::icon(icon::from_name("zoom-original-symbolic")) - .on_press(AppMessage::ZoomReset) - .padding(4), - ) - // Zoom fit button - .push( - button::icon(icon::from_name("zoom-fit-best-symbolic")) - .on_press(AppMessage::ZoomFit) - .padding(4), - ) - // Document dimensions - .push_maybe(if !doc_info.is_empty() { - Some(text(doc_info)) - } else { - None - }) - // Navigation info - .push_maybe(if folder_count == 0 { - None - } else { - Some(text(nav_info)) - }) - .into() -} diff --git a/src/ui/views/format_panel.rs b/src/ui/views/format_panel.rs deleted file mode 100644 index 7863204..0000000 --- a/src/ui/views/format_panel.rs +++ /dev/null @@ -1,137 +0,0 @@ -// SPDX-License-Identifier: GPL-3.0-or-later -// src/app/view/format_panel.rs -// -// Format panel for paper format and orientation selection. - -use cosmic::widget::{column, radio, text}; -use cosmic::Element; - -use crate::ui::model::{AppMode, AppModel, Orientation, PaperFormat}; -use crate::ui::AppMessage; -use crate::fl; - -/// Build the format panel view for the navigation bar. -pub fn view(model: &AppModel) -> Element<'static, AppMessage> { - // Extract values from Transform mode - let (paper_format, orientation) = match &model.mode { - AppMode::Transform { - paper_format, - orientation, - } => (*paper_format, *orientation), - _ => (None, Orientation::default()), - }; - - let mut content = column::with_capacity(20).spacing(12).padding(16); - - // --- Format Section --- - content = content - .push(text::heading(fl!("format-section-title"))) - .push(text::caption(fl!("format-section-subtitle"))); - - // US Letter - content = content.push( - radio( - "US Letter (216 × 279 mm)", - PaperFormat::UsLetter, - paper_format, - AppMessage::SetPaperFormat, - ) - .size(16), - ); - - // ISO A formats - content = content - .push(text::body("ISO A")) - .push( - radio( - PaperFormat::IsoA0.display_name(), - PaperFormat::IsoA0, - paper_format, - AppMessage::SetPaperFormat, - ) - .size(16), - ) - .push( - radio( - PaperFormat::IsoA1.display_name(), - PaperFormat::IsoA1, - paper_format, - AppMessage::SetPaperFormat, - ) - .size(16), - ) - .push( - radio( - PaperFormat::IsoA2.display_name(), - PaperFormat::IsoA2, - paper_format, - AppMessage::SetPaperFormat, - ) - .size(16), - ) - .push( - radio( - PaperFormat::IsoA3.display_name(), - PaperFormat::IsoA3, - paper_format, - AppMessage::SetPaperFormat, - ) - .size(16), - ) - .push( - radio( - PaperFormat::IsoA4.display_name(), - PaperFormat::IsoA4, - paper_format, - AppMessage::SetPaperFormat, - ) - .size(16), - ) - .push( - radio( - PaperFormat::IsoA5.display_name(), - PaperFormat::IsoA5, - paper_format, - AppMessage::SetPaperFormat, - ) - .size(16), - ) - .push( - radio( - PaperFormat::IsoA6.display_name(), - PaperFormat::IsoA6, - paper_format, - AppMessage::SetPaperFormat, - ) - .size(16), - ); - - // --- Orientation Section --- - content = content - .push(cosmic::widget::vertical_space().height(16)) - .push(text::heading(fl!("orientation-section-title"))); - - // Horizontal - content = content.push( - radio( - "Horizontal", - Orientation::Horizontal, - Some(orientation), - AppMessage::SetOrientation, - ) - .size(16), - ); - - // Vertical - content = content.push( - radio( - "Vertical", - Orientation::Vertical, - Some(orientation), - AppMessage::SetOrientation, - ) - .size(16), - ); - - content.into() -} diff --git a/src/ui/views/header.rs b/src/ui/views/header.rs deleted file mode 100644 index 9f53cbe..0000000 --- a/src/ui/views/header.rs +++ /dev/null @@ -1,91 +0,0 @@ -// SPDX-License-Identifier: GPL-3.0-or-later -// src/app/view/header.rs -// -// Header bar content (navigation, rotation, flip). - -use cosmic::iced::Length; -use cosmic::widget::{button, horizontal_space, icon, row}; -use cosmic::Element; - -use crate::ui::message::AppMessage; -use crate::ui::model::AppModel; -use crate::ui::app::ContextPage; -use crate::application::DocumentManager; -use crate::fl; - -/// Build the start (left) side of the header bar. -pub fn start<'a>( - _model: &'a AppModel, - manager: &'a DocumentManager, -) -> Vec> { - let has_doc = manager.current_document().is_some(); - - // Left section: Panel toggle + Menu + Navigation - let left_controls = row() - .spacing(4) - .push( - button::icon(icon::from_name("view-sidebar-start-symbolic")) - .on_press(AppMessage::ToggleNavBar), - //.tooltip(fl!("tooltip-nav-toggle")), - ) - // .push( - // button::icon(icon::from_name("open-menu-symbolic")) - // .on_press(AppMessage::ToggleMainMenu), - // //.tooltip(fl!("menu-main")), - // ) - .push( - button::icon(icon::from_name("go-previous-symbolic")) - .on_press_maybe(has_doc.then_some(AppMessage::PrevDocument)), - //.tooltip(fl!("tooltip-nav-previous")), - ) - .push( - button::icon(icon::from_name("go-next-symbolic")) - .on_press_maybe(has_doc.then_some(AppMessage::NextDocument)), - //.tooltip(fl!("tooltip-nav-next")), - ); - - // Center section: Transformations - let center_controls = row() - .spacing(4) - .push( - button::icon(icon::from_name("object-rotate-left-symbolic")) - .on_press_maybe(has_doc.then_some(AppMessage::RotateCCW)), - //.tooltip(fl!("tooltip-rotate-ccw")), - ) - .push( - button::icon(icon::from_name("object-rotate-right-symbolic")) - .on_press_maybe(has_doc.then_some(AppMessage::RotateCW)), - //.tooltip(fl!("tooltip-rotate-cw")), - ) - .push(horizontal_space().width(Length::Fixed(12.0))) - .push( - button::icon(icon::from_name("object-flip-horizontal-symbolic")) - .on_press_maybe(has_doc.then_some(AppMessage::FlipHorizontal)), - //.tooltip(fl!("tooltip-flip-horizontal")), - ) - .push( - button::icon(icon::from_name("object-flip-vertical-symbolic")) - .on_press_maybe(has_doc.then_some(AppMessage::FlipVertical)), - //.tooltip(fl!("tooltip-flip-vertical")), - ); - - vec![ - left_controls.into(), - center_controls.into(), - horizontal_space().width(Length::Fill).into(), - ] -} - -/// Build the end (right) side of the header bar. -pub fn end<'a>( - _model: &'a AppModel, - _manager: &'a DocumentManager, -) -> Vec> { - vec![ - // Info panel toggle - button::icon(icon::from_name("dialog-information-symbolic")) - .on_press(AppMessage::ToggleContextPage(ContextPage::Properties)) - //.tooltip(fl!("tooltip-info-panel")) - .into(), - ] -} diff --git a/src/ui/views/meta_panel.rs b/src/ui/views/meta_panel.rs deleted file mode 100644 index 025d4fa..0000000 --- a/src/ui/views/meta_panel.rs +++ /dev/null @@ -1,180 +0,0 @@ -// SPDX-License-Identifier: GPL-3.0-or-later -// src/ui/views/meta_panel.rs -// -// Metadata and properties panel for document information. - -use cosmic::iced::{Alignment, Length}; -use cosmic::widget::{button, column, divider, horizontal_space, icon, row, text}; -use cosmic::Element; - -use crate::application::DocumentManager; -use crate::domain::document::core::document::Renderable; -use crate::ui::{AppMessage, AppModel}; -use crate::fl; - -/// Build the metadata/properties panel view. -pub fn view(_model: &AppModel, manager: &DocumentManager) -> Element<'static, AppMessage> { - let mut content = column::with_capacity(16).spacing(8).padding(12); - - // Header with action icons - content = content.push(panel_header(manager)); - - // Display document metadata if available - if let Some(meta) = manager.current_metadata() { - // --- Basic Information Section --- - content = content - .push(section_header(fl!("meta-section-file"))) - .push(meta_row(fl!("meta-filename"), meta.basic.file_name.clone())) - .push(meta_row(fl!("meta-format"), meta.basic.format.clone())); - - // Show dimensions - original from metadata, current if transformed - let original_dims = (meta.basic.width, meta.basic.height); - let current_dims = if let Some(doc) = manager.current_document() { - let info = doc.info(); - (info.width, info.height) - } else { - (0, 0) - }; - - if original_dims != current_dims && current_dims != (0, 0) { - // Dimensions changed (e.g., rotation) - show both - content = content.push(meta_row( - fl!("meta-dimensions"), - format!( - "{} × {} (original: {} × {})", - current_dims.0, current_dims.1, original_dims.0, original_dims.1 - ), - )); - } else { - // No transformation or no document loaded yet - content = content.push(meta_row( - fl!("meta-dimensions"), - meta.basic.resolution_display(), - )); - } - - content = content - .push(meta_row( - fl!("meta-filesize"), - meta.basic.file_size_display(), - )) - .push(meta_row( - fl!("meta-colortype"), - meta.basic.color_type.clone(), - )); - - // --- EXIF Section (if available) --- - if let Some(ref exif) = meta.exif { - let has_exif_data = exif.camera_display().is_some() - || exif.date_time.is_some() - || exif.exposure_time.is_some() - || exif.f_number.is_some() - || exif.iso.is_some() - || exif.focal_length.is_some() - || exif.gps_display().is_some(); - - if has_exif_data { - content = content - .push(divider::horizontal::light()) - .push(section_header(fl!("meta-section-exif"))); - - if let Some(camera) = exif.camera_display() { - content = content.push(meta_row(fl!("meta-camera"), camera)); - } - - if let Some(ref date) = exif.date_time { - content = content.push(meta_row(fl!("meta-datetime"), date.clone())); - } - - if let Some(ref exposure) = exif.exposure_time { - content = content.push(meta_row(fl!("meta-exposure"), exposure.clone())); - } - - if let Some(ref fnumber) = exif.f_number { - content = content.push(meta_row(fl!("meta-aperture"), fnumber.clone())); - } - - if let Some(iso) = exif.iso { - content = content.push(meta_row(fl!("meta-iso"), format!("ISO {}", iso))); - } - - if let Some(ref focal) = exif.focal_length { - content = content.push(meta_row(fl!("meta-focal"), focal.clone())); - } - - if let Some(gps) = exif.gps_display() { - content = content.push(meta_row(fl!("meta-gps"), gps)); - } - } - } - - // --- File Path (at the bottom, less prominent) --- - content = content - .push(divider::horizontal::light()) - .push(meta_row_small( - fl!("meta-path"), - meta.basic.file_path.clone(), - )); - } else { - // No document loaded - content = content - .push(vertical_space()) - .push(text::body(fl!("no-document"))) - .push(vertical_space()); - } - - content.into() -} - -// ============================================================================= -// Helper Components -// ============================================================================= - -/// Panel header with title and action buttons. -fn panel_header(manager: &DocumentManager) -> Element<'static, AppMessage> { - let has_doc = manager.current_document().is_some(); - - row::with_capacity(5) - .spacing(4) - .align_y(Alignment::Center) - .padding([0, 0, 8, 0]) - .push(text::title4(fl!("panel-properties"))) - .push(horizontal_space().width(Length::Fill)) - .push( - button::icon(icon::from_name("image-x-generic-symbolic")) - .tooltip(fl!("action-set-wallpaper")) - .padding(4) - .on_press_maybe(has_doc.then_some(AppMessage::SetAsWallpaper)), - ) - .into() -} - -/// Section header for grouping metadata. -fn section_header(label: String) -> Element<'static, AppMessage> { - text::heading(label).size(14).into() -} - -/// Key-value metadata row. -fn meta_row(label: String, value: String) -> Element<'static, AppMessage> { - column::with_capacity(2) - .spacing(2) - .push(text::caption(format!("{}:", label))) - .push(text::body(value)) - .into() -} - -/// Less prominent metadata row (smaller text). -fn meta_row_small(label: String, value: String) -> Element<'static, AppMessage> { - column::with_capacity(2) - .spacing(2) - .push(text::caption(format!("{}:", label))) - .push(text::caption(value)) - .into() -} - -/// Vertical spacer helper. -fn vertical_space() -> Element<'static, AppMessage> { - cosmic::widget::vertical_space() - .height(Length::Fixed(32.0)) - .into() -} diff --git a/src/ui/views/mod.rs b/src/ui/views/mod.rs deleted file mode 100644 index 439ae5d..0000000 --- a/src/ui/views/mod.rs +++ /dev/null @@ -1,51 +0,0 @@ -// SPDX-License-Identifier: GPL-3.0-or-later -// src/app/view/mod.rs -// -// View module exports. - -pub mod canvas; -pub mod footer; -pub mod format_panel; -pub mod header; -pub mod meta_panel; -pub mod pages_panel; -pub mod panels; - -use cosmic::iced::Length; -use cosmic::widget::container; -use cosmic::{Action, Element}; - -use crate::ui::model::LeftPanel; -use crate::ui::{AppMessage, AppModel}; -use crate::application::DocumentManager; -use crate::config::AppConfig; - -/// Main application view (canvas area). -pub fn view<'a>( - model: &'a AppModel, - manager: &'a DocumentManager, - config: &'a AppConfig, -) -> Element<'a, AppMessage> { - canvas::view(model, manager, config) -} - -/// Navigation bar content (left panel). -/// -/// Shows different panels based on panel state: -/// - `LeftPanel::Thumbnails`: Page thumbnails (multi-page documents) -/// - `None`: Hidden -pub fn nav_bar<'a>( - model: &'a AppModel, - manager: &'a DocumentManager, -) -> Option>> { - match model.panels.left { - None => None, - Some(LeftPanel::Thumbnails) => pages_panel::view(model, manager).map(|panel| { - container(panel.map(Action::App)) - .width(Length::Shrink) - .height(Length::Fill) - .max_width(250) - .into() - }), - } -} diff --git a/src/ui/views/pages_panel.rs b/src/ui/views/pages_panel.rs deleted file mode 100644 index c2ab024..0000000 --- a/src/ui/views/pages_panel.rs +++ /dev/null @@ -1,103 +0,0 @@ -// SPDX-License-Identifier: GPL-3.0-or-later -// src/app/view/pages_panel.rs -// -// Page navigation panel for multi-page documents (PDF, multi-page TIFF, etc.). - -/// Maximum width in pixels for page navigation thumbnails. -const THUMBNAIL_MAX_WIDTH: f32 = 100.0; - -use cosmic::iced::{Alignment, Length}; -use cosmic::widget::{button, column, container, scrollable, text}; -use cosmic::widget::image as cosmic_image; - -use cosmic::Element; - -use crate::application::DocumentManager; -use crate::ui::{AppMessage, AppModel}; -use crate::fl; - -/// Build the page navigation panel view. -/// Returns None if the current document doesn't support multiple pages. -pub fn view<'a>( - _model: &'a AppModel, - manager: &'a DocumentManager, -) -> Option> { - // Get document and check if it's multi-page - let doc = manager.current_document()?; - let page_count = doc.page_count(); - - if page_count <= 1 { - return None; - } - - let current_page = doc.current_page(); - let loaded = doc.thumbnails_loaded(); - - let mut content = column::with_capacity(page_count + 1) - .spacing(12) - .padding([12, 8]) - .align_x(Alignment::Center) - .width(Length::Fill); - - // Show loading progress if not all thumbnails are ready. - if !doc.thumbnails_ready() { - let loading_msg = fl!("loading-thumbnails", current: loaded, total: page_count); - content = content.push(text::caption(loading_msg)); - } - - // Build thumbnail list for pages that are already loaded. - for page_index in 0..loaded { - let is_current = page_index == current_page; - - // Get cached thumbnail handle (read-only access). - let thumbnail_element: Element<'static, AppMessage> = - if let Some(handle) = manager.get_thumbnail_handle(page_index) { - // Display the thumbnail image. - cosmic_image::Image::new(handle) - .width(Length::Fixed(THUMBNAIL_MAX_WIDTH)) - .into() - } else { - // Fallback: show page number if thumbnail not yet loaded. - container(text(format!("Page {}", page_index + 1))) - .width(Length::Fixed(THUMBNAIL_MAX_WIDTH)) - .height(Length::Fixed(THUMBNAIL_MAX_WIDTH * 1.4)) - .center_x(Length::Fill) - .center_y(Length::Fill) - .into() - }; - - // Page number label. - let page_label = text::caption(format!("{}", page_index + 1)); - - // Combine thumbnail and label in a column. - let page_content = column::with_capacity(2) - .spacing(4) - .align_x(Alignment::Center) - .push(thumbnail_element) - .push(page_label); - - // Wrap in button for navigation. - let page_button = if is_current { - // Current page: highlighted style. - button::custom(page_content) - .class(cosmic::theme::Button::Suggested) - .padding(4) - } else { - // Other pages: clickable with standard style. - button::custom(page_content) - .class(cosmic::theme::Button::Standard) - .padding(4) - .on_press(AppMessage::GotoPage(page_index)) - }; - - content = content.push(page_button); - } - - // Wrap in scrollable container. - Some( - scrollable(content) - .width(Length::Shrink) - .height(Length::Fill) - .into(), - ) -} diff --git a/src/ui/views/panels.rs b/src/ui/views/panels.rs deleted file mode 100644 index 5879d8e..0000000 --- a/src/ui/views/panels.rs +++ /dev/null @@ -1,43 +0,0 @@ -// SPDX-License-Identifier: GPL-3.0-or-later -// src/ui/views/panels.rs -// -// Panel router - delegates to specific panel views. - -use cosmic::Element; - -use crate::application::DocumentManager; -use crate::ui::model::{AppModel, RightPanel}; -use crate::ui::AppMessage; - -use super::{format_panel, meta_panel}; - -/// Build the right panel view based on current panel state. -/// -/// Returns the appropriate panel content: -/// - `RightPanel::Properties`: Metadata and document properties (default) -/// - `RightPanel::CropTools`: Crop tool controls (TODO) -/// - `RightPanel::TransformTools`: Transform/export controls -/// -/// Defaults to Properties panel if no panel is explicitly set. -pub fn view(model: &AppModel, manager: &DocumentManager) -> Element<'static, AppMessage> { - match model.panels.right.as_ref() { - Some(RightPanel::Properties) | None => meta_panel::view(model, manager), - Some(RightPanel::CropTools) => crop_tools_panel(model, manager), - Some(RightPanel::TransformTools) => format_panel::view(model), - } -} - -/// Crop tools panel (TODO: implement dedicated crop controls). -fn crop_tools_panel(_model: &AppModel, _manager: &DocumentManager) -> Element<'static, AppMessage> { - use cosmic::widget::{column, text}; - - column::with_capacity(4) - .spacing(12) - .padding(12) - .push(text::title4("Crop Tools")) - .push(text::body("Crop controls will be implemented here.")) - .push(text::caption( - "For now, use the crop overlay on the canvas.", - )) - .into() -} diff --git a/src/ui/widgets/crop_model.rs b/src/ui/widgets/crop_model.rs deleted file mode 100644 index 465e471..0000000 --- a/src/ui/widgets/crop_model.rs +++ /dev/null @@ -1,210 +0,0 @@ -// SPDX-License-Identifier: GPL-3.0-or-later -// src/ui/widgets/crop_model.rs -// -// Crop UI model (drag state and logic). - -use crate::domain::document::operations::CropRegion; - -/// Drag handle for crop selection. -/// -/// Identifies which part of the selection is being dragged. -/// Pure UI concern - not part of domain. -#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)] -pub enum DragHandle { - #[default] - None, - TopLeft, - TopRight, - BottomLeft, - BottomRight, - Top, - Bottom, - Left, - Right, - Move, -} - -/// Crop selection UI model. -/// -/// Manages the interactive state of crop selection: -/// - Current selection region (in screen/canvas coordinates) -/// - Drag state (what's being dragged, where it started) -/// - Drag logic (how to update region based on handle type) -/// -/// This is UI-specific logic, not domain logic! -#[derive(Debug, Clone, Default)] -pub struct CropSelection { - /// Current selection region as (x, y, width, height) in screen coordinates - pub region: Option<(f32, f32, f32, f32)>, - - /// Is user currently dragging? - pub is_dragging: bool, - - /// Which handle/part is being dragged? - pub drag_handle: DragHandle, - - /// Where did the drag start? (for delta calculation) - drag_start: Option<(f32, f32)>, - - /// What was the region when drag started? (for resize calculation) - drag_start_region: Option<(f32, f32, f32, f32)>, -} - -impl CropSelection { - pub fn new() -> Self { - Self::default() - } - - /// Start a new selection (user clicks on empty area). - pub fn start_new_selection(&mut self, x: f32, y: f32) { - self.region = Some((x, y, 0.0, 0.0)); - self.is_dragging = true; - self.drag_handle = DragHandle::None; - self.drag_start = Some((x, y)); - self.drag_start_region = None; - } - - /// Start dragging a handle (user clicks on existing selection). - pub fn start_handle_drag(&mut self, handle: DragHandle, x: f32, y: f32) { - self.is_dragging = true; - self.drag_handle = handle; - self.drag_start = Some((x, y)); - self.drag_start_region = self.region; - } - - /// Update selection during drag. - pub fn update_drag(&mut self, x: f32, y: f32, img_width: f32, img_height: f32) { - if !self.is_dragging { - return; - } - - match self.drag_handle { - DragHandle::None => { - // Creating new selection - expand from start point - if let Some((start_x, start_y)) = self.drag_start { - let min_x = start_x.min(x).max(0.0); - let min_y = start_y.min(y).max(0.0); - let max_x = start_x.max(x).min(img_width); - let max_y = start_y.max(y).min(img_height); - - self.region = Some((min_x, min_y, max_x - min_x, max_y - min_y)); - } - } - DragHandle::Move => { - // Moving entire selection - if let (Some((start_x, start_y)), Some((rx, ry, rw, rh))) = - (self.drag_start, self.drag_start_region) - { - let dx = x - start_x; - let dy = y - start_y; - let new_x = (rx + dx).max(0.0).min(img_width - rw); - let new_y = (ry + dy).max(0.0).min(img_height - rh); - self.region = Some((new_x, new_y, rw, rh)); - } - } - _ => { - // Resizing from edge/corner - if let Some((rx, ry, rw, rh)) = self.drag_start_region { - let (new_x, new_y, new_w, new_h) = - self.resize_region(rx, ry, rw, rh, x, y, img_width, img_height); - self.region = Some((new_x, new_y, new_w, new_h)); - } - } - } - } - - /// Resize region based on which handle is being dragged. - fn resize_region( - &self, - rx: f32, - ry: f32, - rw: f32, - rh: f32, - x: f32, - y: f32, - img_width: f32, - img_height: f32, - ) -> (f32, f32, f32, f32) { - const MIN_SIZE: f32 = 10.0; - - let right = rx + rw; - let bottom = ry + rh; - let x = x.max(0.0).min(img_width); - let y = y.max(0.0).min(img_height); - - match self.drag_handle { - DragHandle::TopLeft => { - let new_x = x.min(right - MIN_SIZE); - let new_y = y.min(bottom - MIN_SIZE); - (new_x, new_y, right - new_x, bottom - new_y) - } - DragHandle::TopRight => { - let new_right = x.max(rx + MIN_SIZE); - let new_y = y.min(bottom - MIN_SIZE); - (rx, new_y, new_right - rx, bottom - new_y) - } - DragHandle::BottomLeft => { - let new_x = x.min(right - MIN_SIZE); - let new_bottom = y.max(ry + MIN_SIZE); - (new_x, ry, right - new_x, new_bottom - ry) - } - DragHandle::BottomRight => { - let new_right = x.max(rx + MIN_SIZE); - let new_bottom = y.max(ry + MIN_SIZE); - (rx, ry, new_right - rx, new_bottom - ry) - } - DragHandle::Top => { - let new_y = y.min(bottom - MIN_SIZE); - (rx, new_y, rw, bottom - new_y) - } - DragHandle::Bottom => { - let new_bottom = y.max(ry + MIN_SIZE); - (rx, ry, rw, new_bottom - ry) - } - DragHandle::Left => { - let new_x = x.min(right - MIN_SIZE); - (new_x, ry, right - new_x, rh) - } - DragHandle::Right => { - let new_right = x.max(rx + MIN_SIZE); - (rx, ry, new_right - rx, rh) - } - _ => (rx, ry, rw, rh), - } - } - - /// End drag operation. - pub fn end_drag(&mut self) { - self.is_dragging = false; - self.drag_start = None; - self.drag_start_region = None; - } - - /// Reset selection (cancel). - pub fn reset(&mut self) { - *self = Self::default(); - } - - /// Check if there's a valid selection. - pub fn has_selection(&self) -> bool { - self.region - .map(|(_, _, w, h)| w > 1.0 && h > 1.0) - .unwrap_or(false) - } - - /// Convert to domain CropRegion for actual crop operation. - pub fn to_crop_region(&self) -> Option { - self.region.and_then(|(x, y, w, h)| { - if w > 1.0 && h > 1.0 { - Some(CropRegion { - x: x as u32, - y: y as u32, - width: w as u32, - height: h as u32, - }) - } else { - None - } - }) - } -} diff --git a/src/ui/widgets/crop_overlay.rs b/src/ui/widgets/crop_overlay.rs deleted file mode 100644 index 457d2ca..0000000 --- a/src/ui/widgets/crop_overlay.rs +++ /dev/null @@ -1,404 +0,0 @@ -// SPDX-License-Identifier: GPL-3.0-or-later -// src/ui/widgets/crop_overlay.rs -// -// Simple crop overlay. - -use cosmic::{ - Element, Renderer, - iced::{ - Color, Length, Point, Rectangle, Size, - advanced::{ - Clipboard, Layout, Shell, Widget, - layout::{Limits, Node}, - renderer::{Quad, Renderer as QuadRenderer}, - widget::Tree, - }, - event::{Event, Status}, - mouse::{self, Button, Cursor}, - }, -}; - -use crate::ui::widgets::crop_model::{CropSelection, DragHandle}; -use crate::ui::AppMessage; - -const HANDLE_SIZE: f32 = 12.0; -const HANDLE_HIT_SIZE: f32 = 24.0; -const OVERLAY_COLOR: Color = Color::from_rgba(0.0, 0.0, 0.0, 0.5); -const HANDLE_COLOR: Color = Color::WHITE; -const BORDER_COLOR: Color = Color::WHITE; -const BORDER_WIDTH: f32 = 2.0; - -pub struct CropOverlay { - selection: CropSelection, - show_grid: bool, - last_click: Option, -} - -impl CropOverlay { - pub fn new(selection: &CropSelection, show_grid: bool) -> Self { - Self { - selection: selection.clone(), - show_grid, - last_click: None, - } - } - - fn hit_test_handle(&self, point: Point) -> DragHandle { - let Some((x, y, w, h)) = self.selection.region else { - return DragHandle::None; - }; - - let handles = [ - (Point::new(x, y), DragHandle::TopLeft), - (Point::new(x + w, y), DragHandle::TopRight), - (Point::new(x, y + h), DragHandle::BottomLeft), - (Point::new(x + w, y + h), DragHandle::BottomRight), - (Point::new(x + w / 2.0, y), DragHandle::Top), - (Point::new(x + w / 2.0, y + h), DragHandle::Bottom), - (Point::new(x, y + h / 2.0), DragHandle::Left), - (Point::new(x + w, y + h / 2.0), DragHandle::Right), - ]; - - for (pos, handle) in handles { - if point_in_handle(point, pos) { - return handle; - } - } - - if point.x >= x && point.x <= x + w && point.y >= y && point.y <= y + h { - return DragHandle::Move; - } - - DragHandle::None - } - - fn draw_overlay(&self, renderer: &mut Renderer, bounds: Rectangle) { - let Some((x, y, w, h)) = self.selection.region else { - draw_quad(renderer, bounds, OVERLAY_COLOR); - return; - }; - - // Convert relative coords to absolute screen coords - let abs_x = bounds.x + x; - let abs_y = bounds.y + y; - let abs_right = abs_x + w; - let abs_bottom = abs_y + h; - - // Top - if abs_y > bounds.y { - draw_quad( - renderer, - Rectangle::new( - Point::new(bounds.x, bounds.y), - Size::new(bounds.width, abs_y - bounds.y), - ), - OVERLAY_COLOR, - ); - } - - // Bottom - if abs_bottom < bounds.y + bounds.height { - draw_quad( - renderer, - Rectangle::new( - Point::new(bounds.x, abs_bottom), - Size::new(bounds.width, bounds.y + bounds.height - abs_bottom), - ), - OVERLAY_COLOR, - ); - } - - // Left - if abs_x > bounds.x { - draw_quad( - renderer, - Rectangle::new( - Point::new(bounds.x, abs_y), - Size::new(abs_x - bounds.x, h), - ), - OVERLAY_COLOR, - ); - } - - // Right - if abs_right < bounds.x + bounds.width { - draw_quad( - renderer, - Rectangle::new( - Point::new(abs_right, abs_y), - Size::new(bounds.x + bounds.width - abs_right, h), - ), - OVERLAY_COLOR, - ); - } - } - - fn draw_border(&self, renderer: &mut Renderer, bounds: Rectangle) { - let Some((x, y, w, h)) = self.selection.region else { - return; - }; - - // Add bounds offset - let abs_x = bounds.x + x; - let abs_y = bounds.y + y; - - // Top - draw_quad( - renderer, - Rectangle::new(Point::new(abs_x, abs_y), Size::new(w, BORDER_WIDTH)), - BORDER_COLOR, - ); - - // Bottom - draw_quad( - renderer, - Rectangle::new( - Point::new(abs_x, abs_y + h - BORDER_WIDTH), - Size::new(w, BORDER_WIDTH), - ), - BORDER_COLOR, - ); - - // Left - draw_quad( - renderer, - Rectangle::new(Point::new(abs_x, abs_y), Size::new(BORDER_WIDTH, h)), - BORDER_COLOR, - ); - - // Right - draw_quad( - renderer, - Rectangle::new( - Point::new(abs_x + w - BORDER_WIDTH, abs_y), - Size::new(BORDER_WIDTH, h), - ), - BORDER_COLOR, - ); - } - - fn draw_handles(&self, renderer: &mut Renderer, bounds: Rectangle) { - let Some((x, y, w, h)) = self.selection.region else { - return; - }; - - let half = HANDLE_SIZE / 2.0; - - // Add bounds offset - let abs_x = bounds.x + x; - let abs_y = bounds.y + y; - - let handles = [ - Point::new(abs_x, abs_y), - Point::new(abs_x + w, abs_y), - Point::new(abs_x, abs_y + h), - Point::new(abs_x + w, abs_y + h), - Point::new(abs_x + w / 2.0, abs_y), - Point::new(abs_x + w / 2.0, abs_y + h), - Point::new(abs_x, abs_y + h / 2.0), - Point::new(abs_x + w, abs_y + h / 2.0), - ]; - - for pos in handles { - draw_quad( - renderer, - Rectangle::new( - Point::new(pos.x - half, pos.y - half), - Size::new(HANDLE_SIZE, HANDLE_SIZE), - ), - HANDLE_COLOR, - ); - } - } - - fn draw_grid(&self, renderer: &mut Renderer, bounds: Rectangle) { - if !self.show_grid { - return; - } - - let Some((x, y, w, h)) = self.selection.region else { - return; - }; - - if w <= 10.0 || h <= 10.0 { - return; - } - - // Add bounds offset - let abs_x = bounds.x + x; - let abs_y = bounds.y + y; - - let grid_color = Color::from_rgba(1.0, 1.0, 1.0, 0.3); - let third_w = w / 3.0; - let third_h = h / 3.0; - - // 2 vertical - for i in 1..3 { - let line_x = abs_x + third_w * i as f32; - draw_quad( - renderer, - Rectangle::new(Point::new(line_x, abs_y), Size::new(1.0, h)), - grid_color, - ); - } - - // 2 horizontal - for i in 1..3 { - let line_y = abs_y + third_h * i as f32; - draw_quad( - renderer, - Rectangle::new(Point::new(abs_x, line_y), Size::new(w, 1.0)), - grid_color, - ); - } - } -} - -impl Widget for CropOverlay { - fn size(&self) -> Size { - Size::new(Length::Fill, Length::Fill) - } - - fn layout(&self, _tree: &mut Tree, _renderer: &Renderer, limits: &Limits) -> Node { - Node::new(limits.max()) - } - - fn draw( - &self, - _tree: &Tree, - renderer: &mut Renderer, - _theme: &cosmic::Theme, - _style: &cosmic::iced::advanced::renderer::Style, - layout: Layout<'_>, - _cursor: Cursor, - _viewport: &Rectangle, - ) { - let bounds = layout.bounds(); - - self.draw_overlay(renderer, bounds); - self.draw_border(renderer, bounds); - self.draw_handles(renderer, bounds); - self.draw_grid(renderer, bounds); - } - - fn on_event( - &mut self, - _tree: &mut Tree, - event: Event, - layout: Layout<'_>, - cursor: Cursor, - _renderer: &Renderer, - _clipboard: &mut dyn Clipboard, - shell: &mut Shell<'_, AppMessage>, - _viewport: &Rectangle, - ) -> Status { - let bounds = layout.bounds(); - - match event { - Event::Mouse(mouse::Event::ButtonPressed(Button::Left)) => { - if let Some(pos) = cursor.position_in(bounds) { - let handle = self.hit_test_handle(pos); - - if handle == DragHandle::Move { - use std::time::{Duration, Instant}; - let now = Instant::now(); - if let Some(last) = self.last_click { - if now.duration_since(last) < Duration::from_millis(400) { - shell.publish(AppMessage::ApplyCrop); - self.last_click = None; - return Status::Captured; - } - } - self.last_click = Some(now); - } - - shell.publish(AppMessage::CropDragStart { - x: pos.x, - y: pos.y, - handle, - }); - return Status::Captured; - } - } - Event::Mouse(mouse::Event::CursorMoved { .. }) => { - if self.selection.is_dragging { - if let Some(pos) = cursor.position_in(bounds) { - shell.publish(AppMessage::CropDragMove { - x: pos.x, - y: pos.y, - max_x: bounds.width, - max_y: bounds.height, - }); - return Status::Captured; - } - } - } - Event::Mouse(mouse::Event::ButtonReleased(Button::Left)) => { - if self.selection.is_dragging { - shell.publish(AppMessage::CropDragEnd); - return Status::Captured; - } - } - _ => {} - } - - Status::Ignored - } - - fn mouse_interaction( - &self, - _tree: &Tree, - layout: Layout<'_>, - cursor: Cursor, - _viewport: &Rectangle, - _renderer: &Renderer, - ) -> mouse::Interaction { - let bounds = layout.bounds(); - - if let Some(pos) = cursor.position_in(bounds) { - let handle = self.hit_test_handle(pos); - return match handle { - DragHandle::TopLeft | DragHandle::BottomRight => { - mouse::Interaction::ResizingDiagonallyDown - } - DragHandle::TopRight | DragHandle::BottomLeft => { - mouse::Interaction::ResizingDiagonallyUp - } - DragHandle::Top | DragHandle::Bottom => mouse::Interaction::ResizingVertically, - DragHandle::Left | DragHandle::Right => mouse::Interaction::ResizingHorizontally, - DragHandle::Move => mouse::Interaction::Grabbing, - DragHandle::None => mouse::Interaction::Crosshair, - }; - } - - mouse::Interaction::None - } -} - -impl<'a> From for Element<'a, AppMessage> { - fn from(widget: CropOverlay) -> Self { - Element::new(widget) - } -} - -fn point_in_handle(point: Point, handle_center: Point) -> bool { - let half = HANDLE_HIT_SIZE / 2.0; - point.x >= handle_center.x - half - && point.x <= handle_center.x + half - && point.y >= handle_center.y - half - && point.y <= handle_center.y + half -} - -fn draw_quad(renderer: &mut Renderer, bounds: Rectangle, color: Color) { - renderer.fill_quad( - Quad { - bounds, - ..Quad::default() - }, - color, - ); -} - -pub fn crop_overlay<'a>(selection: &CropSelection, show_grid: bool) -> Element<'a, AppMessage> { - CropOverlay::new(selection, show_grid).into() -} diff --git a/src/ui/widgets/image_viewer.rs b/src/ui/widgets/image_viewer.rs deleted file mode 100644 index 01a30d9..0000000 --- a/src/ui/widgets/image_viewer.rs +++ /dev/null @@ -1,559 +0,0 @@ -// SPDX-License-Identifier: GPL-3.0-or-later -// src/app/view/image_viewer.rs -// -// Zoom and pan image viewer widget with external state control. -// Forked from cosmic::iced to support external state control. - -use cosmic::iced::advanced::image as img_renderer; -use cosmic::iced::advanced::layout; -use cosmic::iced::advanced::renderer; -use cosmic::iced::advanced::widget::tree::{self, Tree}; -use cosmic::iced::advanced::widget::Widget; -use cosmic::iced::advanced::{Clipboard, Layout, Shell}; -use cosmic::iced::event::{self, Event}; -use cosmic::iced::mouse; -use cosmic::iced::widget::image::FilterMethod; -use cosmic::iced::{ContentFit, Element, Length, Pixels, Point, Radians, Rectangle, Size, Vector}; - -/// Tolerance for scale comparisons in widget state synchronization. -const SCALE_EPSILON: f32 = 0.0001; - -/// Tolerance for offset comparisons in widget state synchronization. -const OFFSET_EPSILON: f32 = 0.01; - -/// Callback type for notifying viewer state changes (scale, `offset_x`, `offset_y`, `canvas_size`, `image_size`). -type StateChangeCallback = Box Message>; - -/// A frame that displays an image with the ability to zoom in/out and pan. -#[allow(missing_debug_implementations)] -pub struct Viewer { - padding: f32, - width: Length, - height: Length, - min_scale: f32, - max_scale: f32, - scale_step: f32, - handle: Handle, - filter_method: FilterMethod, - content_fit: ContentFit, - /// Optional external state to override internal state (scale, offset) - external_state: Option<(f32, Vector)>, - /// Optional callback to notify state changes - on_state_change: Option>, - /// Disable pan interaction (for crop mode) - disable_pan: bool, -} - -impl Viewer { - /// Creates a new [`Viewer`] with the given handle. - pub fn new>(handle: T) -> Self { - Viewer { - handle: handle.into(), - padding: 0.0, - width: Length::Shrink, - height: Length::Shrink, - min_scale: 0.25, - max_scale: 10.0, - scale_step: 0.10, - filter_method: FilterMethod::default(), - content_fit: ContentFit::default(), - external_state: None, - on_state_change: None, - disable_pan: false, - } - } - - /// Set external state to control zoom and pan from outside. - /// This allows keyboard/button controls to override the internal state. - pub fn with_state(mut self, scale: f32, offset_x: f32, offset_y: f32) -> Self { - self.external_state = Some((scale, Vector::new(offset_x, offset_y))); - self - } - - /// Set a callback to be notified when the state changes (for mouse interaction). - pub fn on_state_change(mut self, f: F) -> Self - where - F: 'static + Fn(f32, f32, f32, Size, Size) -> Message, - { - self.on_state_change = Some(Box::new(f)); - self - } - - /// Disable pan interaction (useful when overlaying crop tools). - pub fn disable_pan(mut self, disable: bool) -> Self { - self.disable_pan = disable; - self - } - - /// Sets the [`FilterMethod`] of the [`Viewer`]. - pub fn filter_method(mut self, filter_method: FilterMethod) -> Self { - self.filter_method = filter_method; - self - } - - /// Sets the [`ContentFit`] of the [`Viewer`]. - pub fn content_fit(mut self, content_fit: ContentFit) -> Self { - self.content_fit = content_fit; - self - } - - /// Sets the padding of the [`Viewer`]. - pub fn padding(mut self, padding: impl Into) -> Self { - self.padding = padding.into().0; - self - } - - /// Sets the width of the [`Viewer`]. - pub fn width(mut self, width: impl Into) -> Self { - self.width = width.into(); - self - } - - /// Sets the height of the [`Viewer`]. - pub fn height(mut self, height: impl Into) -> Self { - self.height = height.into(); - self - } - - /// Sets the max scale applied to the image of the [`Viewer`]. - /// - /// Default is `10.0` - pub fn max_scale(mut self, max_scale: f32) -> Self { - self.max_scale = max_scale; - self - } - - /// Sets the min scale applied to the image of the [`Viewer`]. - /// - /// Default is `0.25` - pub fn min_scale(mut self, min_scale: f32) -> Self { - self.min_scale = min_scale; - self - } - - /// Sets the percentage the image of the [`Viewer`] will be scaled by - /// when zoomed in / out. - /// - /// Default is `0.10` - pub fn scale_step(mut self, scale_step: f32) -> Self { - self.scale_step = scale_step; - self - } -} - -impl Widget for Viewer -where - Renderer: img_renderer::Renderer, - Handle: Clone, - Message: Clone, -{ - fn tag(&self) -> tree::Tag { - tree::Tag::of::() - } - - fn state(&self) -> tree::State { - let mut state = State::new(); - // Apply external state if provided at creation - if let Some((scale, offset)) = self.external_state { - state.scale = scale; - state.current_offset = offset; - state.starting_offset = offset; - } - tree::State::new(state) - } - - fn diff(&mut self, tree: &mut Tree) { - // Sync external state into internal state when user is not dragging - if let Some((ext_scale, ext_offset)) = self.external_state { - let state = tree.state.downcast_mut::(); - - // Only apply external state if user is not currently dragging - if !state.is_cursor_grabbed() { - // Check if external state differs significantly from current state - let scale_changed = (state.scale - ext_scale).abs() > SCALE_EPSILON; - let offset_changed = (state.current_offset.x - ext_offset.x).abs() > OFFSET_EPSILON - || (state.current_offset.y - ext_offset.y).abs() > OFFSET_EPSILON; - - if scale_changed || offset_changed { - state.scale = ext_scale; - state.current_offset = ext_offset; - state.starting_offset = ext_offset; - } - } - } - } - - fn size(&self) -> Size { - Size { - width: self.width, - height: self.height, - } - } - - fn layout( - &self, - _tree: &mut Tree, - renderer: &Renderer, - limits: &layout::Limits, - ) -> layout::Node { - let image_size = renderer.measure_image(&self.handle); - let image_size = Size::new(image_size.width as f32, image_size.height as f32); - - let raw_size = limits.resolve(self.width, self.height, image_size); - let full_size = self.content_fit.fit(image_size, raw_size); - - let final_size = Size { - width: match self.width { - Length::Shrink => f32::min(raw_size.width, full_size.width), - _ => raw_size.width, - }, - height: match self.height { - Length::Shrink => f32::min(raw_size.height, full_size.height), - _ => raw_size.height, - }, - }; - - layout::Node::new(final_size) - } - - fn on_event( - &mut self, - tree: &mut Tree, - event: Event, - layout: Layout<'_>, - cursor: mouse::Cursor, - renderer: &Renderer, - _clipboard: &mut dyn Clipboard, - shell: &mut Shell<'_, Message>, - _viewport: &Rectangle, - ) -> event::Status { - let bounds = layout.bounds(); - - match event { - Event::Mouse(mouse::Event::WheelScrolled { delta }) => { - let Some(cursor_position) = cursor.position_over(bounds) else { - return event::Status::Ignored; - }; - - match delta { - mouse::ScrollDelta::Lines { y, .. } | mouse::ScrollDelta::Pixels { y, .. } => { - let state = tree.state.downcast_mut::(); - let previous_scale = state.scale; - - if y < 0.0 && previous_scale > self.min_scale - || y > 0.0 && previous_scale < self.max_scale - { - state.scale = (if y > 0.0 { - state.scale * (1.0 + self.scale_step) - } else { - state.scale / (1.0 + self.scale_step) - }) - .clamp(self.min_scale, self.max_scale); - - let scale_factor = state.scale / previous_scale; - - // Cursor position relative to the image center (not bounds center) - // The image is centered in bounds, so bounds.center() is correct - let cursor_to_center = cursor_position - bounds.center(); - - // Transform offset so the point under cursor stays stationary - // Formula: new_offset = old_offset * scale_factor + cursor_to_center * (scale_factor - 1) - let new_offset = Vector::new( - state.current_offset.x * scale_factor - + cursor_to_center.x * (scale_factor - 1.0), - state.current_offset.y * scale_factor - + cursor_to_center.y * (scale_factor - 1.0), - ); - - // Clamp offset to valid range - let scaled_size = scaled_image_size( - renderer, - &self.handle, - state, - bounds.size(), - self.content_fit, - ); - - state.current_offset = - clamp_offset(new_offset, bounds.size(), scaled_size); - - // Notify state change - if let Some(ref on_change) = self.on_state_change { - let image_size = renderer.measure_image(&self.handle); - let image_size = - Size::new(image_size.width as f32, image_size.height as f32); - shell.publish(on_change( - state.scale, - state.current_offset.x, - state.current_offset.y, - bounds.size(), - image_size, - )); - } - } - } - } - - event::Status::Captured - } - Event::Mouse(mouse::Event::ButtonPressed(mouse::Button::Left)) => { - if self.disable_pan { - return event::Status::Ignored; - } - - let Some(cursor_position) = cursor.position_over(bounds) else { - return event::Status::Ignored; - }; - - let state = tree.state.downcast_mut::(); - state.cursor_grabbed_at = Some(cursor_position); - state.starting_offset = state.current_offset; - - event::Status::Captured - } - Event::Mouse(mouse::Event::ButtonReleased(mouse::Button::Left)) => { - if self.disable_pan { - return event::Status::Ignored; - } - - let state = tree.state.downcast_mut::(); - - if state.cursor_grabbed_at.is_some() { - state.cursor_grabbed_at = None; - - // Notify final state after drag ends - if let Some(ref on_change) = self.on_state_change { - let image_size = renderer.measure_image(&self.handle); - let image_size = - Size::new(image_size.width as f32, image_size.height as f32); - shell.publish(on_change( - state.scale, - state.current_offset.x, - state.current_offset.y, - bounds.size(), - image_size, - )); - } - - event::Status::Captured - } else { - event::Status::Ignored - } - } - Event::Mouse(mouse::Event::CursorMoved { position }) => { - if self.disable_pan { - return event::Status::Ignored; - } - - let state = tree.state.downcast_mut::(); - - if let Some(origin) = state.cursor_grabbed_at { - let scaled_size = scaled_image_size( - renderer, - &self.handle, - state, - bounds.size(), - self.content_fit, - ); - - let delta = position - origin; - - // Pan: subtract delta from starting offset - let new_offset = Vector::new( - state.starting_offset.x - delta.x, - state.starting_offset.y - delta.y, - ); - - state.current_offset = clamp_offset(new_offset, bounds.size(), scaled_size); - - // Notify state change during pan - if let Some(ref on_change) = self.on_state_change { - let image_size = renderer.measure_image(&self.handle); - let image_size = - Size::new(image_size.width as f32, image_size.height as f32); - shell.publish(on_change( - state.scale, - state.current_offset.x, - state.current_offset.y, - bounds.size(), - image_size, - )); - } - - event::Status::Captured - } else { - event::Status::Ignored - } - } - _ => event::Status::Ignored, - } - } - - fn mouse_interaction( - &self, - tree: &Tree, - layout: Layout<'_>, - cursor: mouse::Cursor, - _viewport: &Rectangle, - _renderer: &Renderer, - ) -> mouse::Interaction { - let state = tree.state.downcast_ref::(); - let bounds = layout.bounds(); - let is_mouse_over = cursor.is_over(bounds); - - if state.is_cursor_grabbed() { - mouse::Interaction::Grabbing - } else if is_mouse_over { - mouse::Interaction::Grab - } else { - mouse::Interaction::None - } - } - - fn draw( - &self, - tree: &Tree, - renderer: &mut Renderer, - _theme: &Theme, - _style: &renderer::Style, - layout: Layout<'_>, - _cursor: mouse::Cursor, - _viewport: &Rectangle, - ) { - let state = tree.state.downcast_ref::(); - let bounds = layout.bounds(); - - let scaled_size = scaled_image_size( - renderer, - &self.handle, - state, - bounds.size(), - self.content_fit, - ); - - // Calculate translation to center the image and apply offset - let translation = { - // How much space is left after placing the scaled image - let diff_w = bounds.width - scaled_size.width; - let diff_h = bounds.height - scaled_size.height; - - // Base position: center the image in the viewport - // For images smaller than viewport: center them (diff > 0) - // For images larger than viewport: they extend beyond bounds (diff < 0) - let center_offset = Vector::new(diff_w / 2.0, diff_h / 2.0); - - // Apply pan offset (offset moves the "camera", so subtract it) - // Positive offset = looking at right/bottom part = image moves left/up - center_offset - state.current_offset - }; - - let drawing_bounds = Rectangle::new(bounds.position(), scaled_size); - - let render = |renderer: &mut Renderer| { - renderer.with_translation(translation, |renderer| { - renderer.draw_image( - self.handle.clone(), - self.filter_method, - drawing_bounds, - Radians(0.0), - 1.0, - [0.0; 4], - ); - }); - }; - - renderer.with_layer(bounds, render); - } -} - -/// The local state of a [`Viewer`]. -#[derive(Debug, Clone, Copy)] -pub struct State { - scale: f32, - starting_offset: Vector, - current_offset: Vector, - cursor_grabbed_at: Option, -} - -impl Default for State { - fn default() -> Self { - Self { - scale: 1.0, - starting_offset: Vector::default(), - current_offset: Vector::default(), - cursor_grabbed_at: None, - } - } -} - -impl State { - /// Creates a new [`State`]. - pub fn new() -> Self { - State::default() - } - - /// Returns if the cursor is currently grabbed by the [`Viewer`]. - pub fn is_cursor_grabbed(&self) -> bool { - self.cursor_grabbed_at.is_some() - } -} - -/// Clamps the offset to keep the image within reasonable bounds. -/// -/// The offset represents how far the viewport's center is displaced from the image's center. -/// - offset (0, 0) = image centered -/// - positive offset = viewing right/bottom part of image -/// - negative offset = viewing left/top part of image -fn clamp_offset(offset: Vector, viewport_size: Size, image_size: Size) -> Vector { - // Maximum allowed offset in each direction - // When image is larger than viewport, allow panning up to image edge - // When image is smaller than viewport, no panning needed (clamp to 0) - let max_offset_x = ((image_size.width - viewport_size.width) / 2.0).max(0.0); - let max_offset_y = ((image_size.height - viewport_size.height) / 2.0).max(0.0); - - Vector::new( - offset.x.clamp(-max_offset_x, max_offset_x), - offset.y.clamp(-max_offset_y, max_offset_y), - ) -} - -impl<'a, Message, Theme, Renderer, Handle> From> - for Element<'a, Message, Theme, Renderer> -where - Renderer: 'a + img_renderer::Renderer, - Message: 'a + Clone, - Handle: Clone + 'a, -{ - fn from(viewer: Viewer) -> Element<'a, Message, Theme, Renderer> { - Element::new(viewer) - } -} - -/// Returns the scaled size of the image given current state. -/// Calculate the scaled image size after applying content fit and zoom. -/// -/// This is the canonical implementation used by the viewer widget. -/// A simplified version exists in `document::utils::scaled_image_size`. -pub fn scaled_image_size( - renderer: &Renderer, - handle: &::Handle, - state: &State, - bounds: Size, - content_fit: ContentFit, -) -> Size -where - Renderer: img_renderer::Renderer, -{ - let Size { width, height } = renderer.measure_image(handle); - let image_size = Size::new(width as f32, height as f32); - - let adjusted_fit = match content_fit { - ContentFit::None => image_size, - _ => content_fit.fit(image_size, bounds), - }; - - Size::new( - adjusted_fit.width * state.scale, - adjusted_fit.height * state.scale, - ) -} diff --git a/src/ui/widgets/mod.rs b/src/ui/widgets/mod.rs deleted file mode 100644 index fc0a669..0000000 --- a/src/ui/widgets/mod.rs +++ /dev/null @@ -1,13 +0,0 @@ -// SPDX-License-Identifier: GPL-3.0-or-later -// src/ui/widgets/mod.rs -// -// Custom widgets module. - -pub mod crop_model; -pub mod crop_overlay; -pub mod image_viewer; - -// Re-exports for convenience -pub use crop_model::{CropSelection, DragHandle}; -pub use crop_overlay::crop_overlay; -pub use image_viewer::Viewer; diff --git a/src/workspace/manager.rs b/src/workspace/manager.rs new file mode 100644 index 0000000..3b17348 --- /dev/null +++ b/src/workspace/manager.rs @@ -0,0 +1,174 @@ +// SPDX-License-Identifier: GPL-3.0-or-later +// src/workspace/manager.rs +// +// Workspace operations: session and item CRUD, reordering, and RON persistence. + +use crate::workspace::model::{Item, Session, Workspace}; +use anyhow::{Context, Result}; +use std::path::Path; +use uuid::Uuid; + +impl Workspace { + /// Serialize the workspace to a RON file. + pub fn save(&self, path: impl AsRef) -> Result<()> { + let config = ron::ser::PrettyConfig::default(); + let data = + ron::ser::to_string_pretty(self, config).context("Failed to serialize workspace")?; + std::fs::write(&path, data).context("Failed to write workspace file")?; + Ok(()) + } + + /// Deserialize a workspace from a RON file. + pub fn load(path: impl AsRef) -> Result { + let data = std::fs::read_to_string(&path).context("Failed to read workspace file")?; + let workspace: Workspace = + ron::from_str(&data).context("Failed to deserialize workspace")?; + Ok(workspace) + } + + pub fn add_session(&mut self, session: Session) { + self.sessions.push(session); + } + + pub fn remove_session(&mut self, session_id: Uuid) -> Result { + let pos = self.session_pos(session_id)?; + Ok(self.sessions.remove(pos)) + } + + pub fn copy_session(&mut self, session_id: Uuid) -> Result { + let pos = self.session_pos(session_id)?; + let mut clone = self.sessions[pos].clone(); + clone.id = Uuid::new_v4(); + clone.name = format!("{} (copy)", clone.name); + for item in &mut clone.items { + item.id = Uuid::new_v4(); + } + let new_id = clone.id; + self.sessions.push(clone); + Ok(new_id) + } + + pub fn move_session(&mut self, session_id: Uuid, new_index: usize) -> Result<()> { + let pos = self.session_pos(session_id)?; + let session = self.sessions.remove(pos); + let insert_at = new_index.min(self.sessions.len()); + self.sessions.insert(insert_at, session); + Ok(()) + } + + pub fn rename_session(&mut self, session_id: Uuid, new_name: impl Into) -> Result<()> { + let session = self.session_mut(session_id)?; + session.name = new_name.into(); + Ok(()) + } + + pub fn add_item(&mut self, session_id: Uuid, item: Item) -> Result<()> { + let session = self.session_mut(session_id)?; + session.items.push(item); + Ok(()) + } + + pub fn remove_item(&mut self, session_id: Uuid, item_id: Uuid) -> Result { + let session = self.session_mut(session_id)?; + let pos = item_pos(&session.items, item_id)?; + Ok(session.items.remove(pos)) + } + + /// Move an item within the same session or between two sessions. + pub fn move_item( + &mut self, + from_session: Uuid, + item_id: Uuid, + to_session: Uuid, + to_index: usize, + ) -> Result<()> { + let item = self.remove_item(from_session, item_id)?; + let target = self.session_mut(to_session)?; + let insert_at = to_index.min(target.items.len()); + target.items.insert(insert_at, item); + Ok(()) + } + + /// Copy an item within the same session or between two sessions. + pub fn copy_item( + &mut self, + from_session: Uuid, + item_id: Uuid, + to_session: Uuid, + to_index: usize, + ) -> Result { + let source = self.session(from_session)?; + let pos = item_pos(&source.items, item_id)?; + let mut clone = source.items[pos].clone(); + clone.id = Uuid::new_v4(); + clone.name = format!("{} (copy)", clone.name); + let new_id = clone.id; + + let target = self.session_mut(to_session)?; + let insert_at = to_index.min(target.items.len()); + target.items.insert(insert_at, clone); + Ok(new_id) + } + + pub fn rename_item( + &mut self, + session_id: Uuid, + item_id: Uuid, + new_name: impl Into, + ) -> Result<()> { + let session = self.session_mut(session_id)?; + let pos = item_pos(&session.items, item_id)?; + session.items[pos].name = new_name.into(); + Ok(()) + } + + /// Read-only access to a session by id. + pub fn session(&self, id: Uuid) -> Result<&Session> { + self.sessions + .iter() + .find(|s| s.id == id) + .context(format!("Session {id} not found")) + } + + /// Read-only access to a session by name. + pub fn session_by_name(&self, name: &str) -> Result<&Session> { + self.sessions + .iter() + .find(|s| s.name == name) + .context(format!("Session {name:?} not found")) + } + + /// Find an item by name within a session. + pub fn item_by_name(&self, session_id: Uuid, name: &str) -> Result<&Item> { + let session = self.session(session_id)?; + session + .items + .iter() + .find(|i| i.name == name) + .context(format!( + "Item {name:?} not found in session {:?}", + session.name + )) + } + + fn session_mut(&mut self, id: Uuid) -> Result<&mut Session> { + self.sessions + .iter_mut() + .find(|s| s.id == id) + .context(format!("Session {id} not found")) + } + + fn session_pos(&self, id: Uuid) -> Result { + self.sessions + .iter() + .position(|s| s.id == id) + .context(format!("Session {id} not found")) + } +} + +fn item_pos(items: &[Item], id: Uuid) -> Result { + items + .iter() + .position(|i| i.id == id) + .context(format!("Item {id} not found")) +} diff --git a/src/workspace/mod.rs b/src/workspace/mod.rs new file mode 100644 index 0000000..330ed70 --- /dev/null +++ b/src/workspace/mod.rs @@ -0,0 +1,7 @@ +// SPDX-License-Identifier: GPL-3.0-or-later +// src/workspace/mod.rs +// +// Workspace subsystem: data model, operations, and persistence. + +pub mod manager; +pub mod model; diff --git a/src/workspace/model.rs b/src/workspace/model.rs new file mode 100644 index 0000000..4d0e008 --- /dev/null +++ b/src/workspace/model.rs @@ -0,0 +1,100 @@ +// SPDX-License-Identifier: GPL-3.0-or-later +// src/workspace/model.rs +// +// Pure data structures for the workspace, sessions, and their items. + +use serde::{Deserialize, Serialize}; +use std::path::PathBuf; +use uuid::Uuid; + +/// The document kind an item references. +/// +/// Each variant carries the path to the source file. Format-specific fields +/// (e.g. page index for PDFs) live on the variant so they can differ per kind. +#[derive(Debug, Clone, Serialize, Deserialize)] +pub enum ItemKind { + Portable { path: PathBuf }, + Raster { path: PathBuf }, + Vector { path: PathBuf }, + Document { path: PathBuf }, +} + +impl ItemKind { + /// Returns the file path regardless of the variant. + pub fn path(&self) -> &PathBuf { + match self { + ItemKind::Portable { path } + | ItemKind::Raster { path } + | ItemKind::Vector { path } + | ItemKind::Document { path } => path, + } + } +} + +/// A single entry in a session. +#[derive(Debug, Clone, Serialize, Deserialize)] +pub struct Item { + pub id: Uuid, + pub name: String, + pub kind: ItemKind, + pub tags: Vec, +} + +impl Item { + pub fn new(name: impl Into, kind: ItemKind) -> Self { + Self { + id: Uuid::new_v4(), + name: name.into(), + kind, + tags: Vec::new(), + } + } + + pub fn with_tags(mut self, tags: Vec) -> Self { + self.tags = tags; + self + } +} + +/// Determines how a session behaves in the UI. +#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)] +pub enum SessionType { + Browser, + Collection, +} + +/// A named group of items inside a workspace. +#[derive(Debug, Clone, Serialize, Deserialize)] +pub struct Session { + pub id: Uuid, + pub name: String, + pub session_type: SessionType, + pub items: Vec, +} + +impl Session { + pub fn new(name: impl Into, session_type: SessionType) -> Self { + Self { + id: Uuid::new_v4(), + name: name.into(), + session_type, + items: Vec::new(), + } + } +} + +/// The top-level container that is persisted as a `.ws.ron` file. +#[derive(Debug, Clone, Serialize, Deserialize)] +pub struct Workspace { + pub name: String, + pub sessions: Vec, +} + +impl Workspace { + pub fn new(name: impl Into) -> Self { + Self { + name: name.into(), + sessions: Vec::new(), + } + } +} diff --git a/test.json b/test.json new file mode 100644 index 0000000..4dd1247 --- /dev/null +++ b/test.json @@ -0,0 +1,94 @@ +[ + { "NewWorkspace": "Demo Projekt" }, + + { "CreateSession": { "name": "Referenzen", "session_type": "browser" } }, + { + "AddItem": { + "session": "Referenzen", + "name": "Logo", + "kind": "vector", + "path": "tmp/pictures/logo.svg" + } + }, + { + "AddItem": { + "session": "Referenzen", + "name": "Foto Hintergrund", + "kind": "raster", + "path": "tmp/pictures/hero.jpg" + } + }, + { + "AddItem": { + "session": "Referenzen", + "name": "Handbuch", + "kind": "portable", + "path": "tmp/pictures/handbook.pdf" + } + }, + { "ViewSession": "Referenzen" }, + + { "SelectSession": "Referenzen" }, + { "SelectItem": 0 }, + "NextItem", + "NextItem", + "PreviousItem", + { "SelectItem": 0 }, + "MoveItemDown", + { "ViewSession": "Referenzen" }, + "MoveItemUp", + { "ViewSession": "Referenzen" }, + { "MoveItemToIndex": 2 }, + { "ViewSession": "Referenzen" }, + { "MoveItemToIndex": 0 }, + + { "CreateSession": { "name": "Favoriten", "session_type": "collection" } }, + { "CreateSession": { "name": "Archiv", "session_type": "collection" } }, + "ViewWorkspace", + + { "CopyItem": { "from": "Referenzen", "item": "Logo", "to": "Favoriten" } }, + { + "CopyItem": { + "from": "Referenzen", + "item": "Foto Hintergrund", + "to": "Favoriten" + } + }, + { "ViewSession": "Favoriten" }, + + { + "RenameItem": { + "session": "Favoriten", + "item": "Logo (copy)", + "name": "Hauptlogo" + } + }, + { "ViewSession": "Favoriten" }, + + { + "RemoveItem": { "session": "Favoriten", "item": "Foto Hintergrund (copy)" } + }, + { "ViewSession": "Favoriten" }, + + { "MoveItem": { "from": "Referenzen", "item": "Handbuch", "to": "Archiv" } }, + { "ViewSession": "Referenzen" }, + { "ViewSession": "Archiv" }, + + { "CopySession": "Referenzen" }, + "ViewWorkspace", + + { + "RenameSession": { + "session": "Referenzen (copy)", + "name": "Backup Referenzen" + } + }, + "ViewWorkspace", + + { "DeleteSession": "Archiv" }, + "ViewWorkspace", + + { "SaveWorkspace": "tmp/demo.ws.ron" }, + { "LoadWorkspace": "tmp/demo.ws.ron" }, + "ViewWorkspace" +]