diff --git a/Cargo.lock b/Cargo.lock index 9b6ea63..ca8fec5 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -20,43 +20,60 @@ checksum = "366ffbaa4442f4684d91e2cd7c5ea7c4ed8add41959a31447066e279e432b618" [[package]] name = "accesskit" -version = "0.21.1" +version = "0.24.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "cf203f9d3bd8f29f98833d1fbef628df18f759248a547e7e01cfbf63cda36a99" +checksum = "d3b7f7f85a7e5f68090000ed7622545829afd484d210358702ae4cb97dd0c320" +dependencies = [ + "uuid", +] [[package]] name = "accesskit_atspi_common" -version = "0.14.2" +version = "0.19.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "890d241cf51fc784f0ac5ac34dfc847421f8d39da6c7c91a0fcc987db62a8267" +checksum = "7e98018dbef3583d751dbb96e07b8728fb99581360e1c3df408af16f4a80b821" dependencies = [ "accesskit", "accesskit_consumer", "atspi-common", + "phf", "serde", - "thiserror 1.0.69", "zvariant", ] [[package]] name = "accesskit_consumer" -version = "0.31.0" +version = "0.37.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "db81010a6895d8707f9072e6ce98070579b43b717193d2614014abd5cb17dd43" +checksum = "f950720ce064757a1b629caad3a408e8d2c63bb01f29b8a3ff8daa331053ffeb" dependencies = [ "accesskit", - "hashbrown 0.15.5", + "hashbrown 0.16.1", +] + +[[package]] +name = "accesskit_ios" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "02ecb52198c7cf5f8d3e9ffc03d2ca0a5c7201926befd96721437829da4c5c6a" +dependencies = [ + "accesskit", + "accesskit_consumer", + "hashbrown 0.16.1", + "objc2 0.5.2", + "objc2-foundation 0.2.2", + "objc2-ui-kit 0.2.2", ] [[package]] name = "accesskit_macos" -version = "0.22.2" +version = "0.26.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a0089e5c0ac0ca281e13ea374773898d9354cc28d15af9f0f7394d44a495b575" +checksum = "17cb8b66cef272d48161b02a6317cc2bdd5f98bb0a5e79c68f704a5862aa396b" dependencies = [ "accesskit", "accesskit_consumer", - "hashbrown 0.15.5", + "hashbrown 0.16.1", "objc2 0.5.2", "objc2-app-kit 0.2.2", "objc2-foundation 0.2.2", @@ -64,9 +81,9 @@ dependencies = [ [[package]] name = "accesskit_unix" -version = "0.17.2" +version = "0.22.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "301e55b39cfc15d9c48943ce5f572204a551646700d0e8efa424585f94fec528" +checksum = "5376ba4cc23312587634abb5250b1ce8618f01a55915608209aafd01efb4bf8c" dependencies = [ "accesskit", "accesskit_atspi_common", @@ -82,25 +99,26 @@ dependencies = [ [[package]] name = "accesskit_windows" -version = "0.29.2" +version = "0.33.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d2d63dd5041e49c363d83f5419a896ecb074d309c414036f616dc0b04faca971" +checksum = "36e93ac7bf50b964f1cbb75f741629a4e950571baa1ef1274457ab5a80d9bcc2" dependencies = [ "accesskit", "accesskit_consumer", - "hashbrown 0.15.5", + "hashbrown 0.16.1", "static_assertions", - "windows 0.61.3", - "windows-core 0.61.2", + "windows", + "windows-core", ] [[package]] name = "accesskit_winit" -version = "0.29.2" +version = "0.33.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c8cfabe59d0eaca7412bfb1f70198dd31e3b0496fee7e15b066f9c36a1a140a0" +checksum = "41fe5862066316f6caaf02cd3aecd54bced25503ac5dbbfd0d03a42bc1246217" dependencies = [ "accesskit", + "accesskit_ios", "accesskit_macos", "accesskit_unix", "accesskit_windows", @@ -146,6 +164,30 @@ dependencies = [ "memchr", ] +[[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 = "allocator-api2" +version = "0.2.21" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "683d7910e743518b0e34f1186f92494becacb047c7b6bf616c96772180fef923" + [[package]] name = "alsa" version = "0.11.0" @@ -202,6 +244,23 @@ dependencies = [ "libc", ] +[[package]] +name = "annotate-snippets" +version = "0.12.16" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f211a51805bc641f3ad5b7664c77d2547af685cc33b4cd8d31964027a46f13f1" +dependencies = [ + "anstyle", + "memchr", + "unicode-width", +] + +[[package]] +name = "anstyle" +version = "1.0.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "940b3a0ca603d1eade50a4846a2afffd5ef57a9feac2c0e2ec2e14f9ead76000" + [[package]] name = "anyhow" version = "1.0.103" @@ -215,23 +274,20 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "fb5dfbc6d8d2675589ccbe4d0fd61df2419075625f8c1a62325e718e2b0049f9" [[package]] -name = "arboard" -version = "3.6.1" +name = "arbitrary" +version = "1.4.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0348a1c054491f4bfe6ab86a7b6ab1e44e45d899005de92f58b3df180b36ddaf" +checksum = "c3d036a3c4ab069c7b410a2ce876bd74808d2d0888a82667669f8e783a898bf1" + +[[package]] +name = "arg_enum_proc_macro" +version = "0.3.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0ae92a5119aa49cdbcf6b9f893fe4e1d98b04ccbf82ee0584ad948a44a734dea" dependencies = [ - "clipboard-win", - "image", - "log", - "objc2 0.6.4", - "objc2-app-kit 0.3.2", - "objc2-core-foundation", - "objc2-core-graphics", - "objc2-foundation 0.3.2", - "parking_lot", - "percent-encoding", - "windows-sys 0.60.2", - "x11rb", + "proc-macro2", + "quote", + "syn", ] [[package]] @@ -271,12 +327,12 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "175571dd1d178ced59193a6fc02dde1b972eb0bc56c892cde9beeceac5bf0f6b" [[package]] -name = "ash" -version = "0.38.0+1.3.281" +name = "as-slice" +version = "0.2.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0bb44936d800fea8f016d7f2311c6a4f97aebd5dc86f09906139ec848cf3a46f" +checksum = "516b6b4f0e40d50dcda9365d53964ec74560ad4284da2e7fc97122cd83174516" dependencies = [ - "libloading", + "stable_deref_trait", ] [[package]] @@ -418,20 +474,19 @@ checksum = "1505bd5d3d116872e7271a6d4e16d81d0c8570876c8de68093a09ac269d8aac0" [[package]] name = "atspi" -version = "0.25.0" +version = "0.29.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c83247582e7508838caf5f316c00791eee0e15c0bf743e6880585b867e16815c" +checksum = "c77886257be21c9cd89a4ae7e64860c6f0eefca799bb79127913052bd0eefb3d" dependencies = [ "atspi-common", - "atspi-connection", "atspi-proxies", ] [[package]] name = "atspi-common" -version = "0.9.0" +version = "0.13.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "33dfc05e7cdf90988a197803bf24f5788f94f7c94a69efa95683e8ffe76cfdfb" +checksum = "20c5617155740c98003016429ad13fe43ce7a77b007479350a9f8bf95a29f63d" dependencies = [ "enumflags2", "serde", @@ -443,23 +498,11 @@ dependencies = [ "zvariant", ] -[[package]] -name = "atspi-connection" -version = "0.9.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4193d51303d8332304056ae0004714256b46b6635a5c556109b319c0d3784938" -dependencies = [ - "atspi-common", - "atspi-proxies", - "futures-lite", - "zbus", -] - [[package]] name = "atspi-proxies" -version = "0.9.0" +version = "0.13.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d2eebcb9e7e76f26d0bcfd6f0295e1cd1e6f33bedbc5698a971db8dc43d7751c" +checksum = "2230e48787ed3eb4088996eab66a32ca20c0b67bbd4fd6cdfe79f04f1f04c9fc" dependencies = [ "atspi-common", "serde", @@ -477,12 +520,73 @@ dependencies = [ "pkg-config", ] +[[package]] +name = "auto_enums" +version = "0.8.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2e4487600931c9a89f8db7ffbdf3fbdd45bb7bd85e26861f659a463cd0dff966" +dependencies = [ + "derive_utils", + "proc-macro2", + "quote", + "syn", +] + [[package]] name = "autocfg" version = "1.5.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "f2032f911046de80f0a198e0901378627c33f59ea0ac00e363d481118bd70a53" +[[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", + "log", + "num-rational", + "num-traits", + "pastey 0.1.1", + "rayon", + "thiserror 2.0.18", + "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", + "log", + "nom 8.0.0", + "num-rational", + "v_frame", +] + +[[package]] +name = "avif-serialize" +version = "0.8.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e7178fe5f7d460b13895ebb9dcb28a3a6216d2df2574a0806cb51b555d297f38" +dependencies = [ + "arrayvec", +] + +[[package]] +name = "base64" +version = "0.22.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "72b3254f16251a8381aa12e40e3c4d2f0199f8c6508fbecb9d91f575e0fbb8c6" + [[package]] name = "base64ct" version = "1.8.3" @@ -490,19 +594,40 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "2af50177e190e07a26ab74f8b1efbfe2ef87da2116221318cb1c2e82baf7de06" [[package]] -name = "bit-set" -version = "0.8.0" +name = "bincode" +version = "2.0.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "08807e080ed7f9d5433fa9b275196cfc35414f66a0c79d864dc51a0d825231a3" +checksum = "36eaf5d7b090263e8150820482d5d93cd964a81e4019913c972f4edcc6edb740" dependencies = [ - "bit-vec", + "serde", + "unty", ] [[package]] -name = "bit-vec" -version = "0.8.0" +name = "bindgen" +version = "0.72.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5e764a1d40d510daf35e07be9eb06e75770908c27d411ee6c92109c9840eaaf7" +checksum = "993776b509cfb49c750f11b8f07a46fa23e0a1386ffc01fb1e7d343efc387895" +dependencies = [ + "bitflags 2.13.0", + "cexpr", + "clang-sys", + "itertools 0.13.0", + "log", + "prettyplease", + "proc-macro2", + "quote", + "regex", + "rustc-hash 2.1.3", + "shlex 1.3.0", + "syn", +] + +[[package]] +name = "bit_field" +version = "0.10.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1e4b40c7323adcfc0a41c4b88143ed58346ff65a288fc144329c5c45e05d70c6" [[package]] name = "bitflags" @@ -515,6 +640,18 @@ name = "bitflags" version = "2.13.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b4388bee8683e3d04af747c73422af53102d2bd24d9eadb6cbc100baef4b43f8" +dependencies = [ + "serde_core", +] + +[[package]] +name = "bitstream-io" +version = "4.10.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7eff00be299a18769011411c9def0d827e8f2d7bf0c3dbf53633147a8867fd1f" +dependencies = [ + "no_std_io2", +] [[package]] name = "blake2" @@ -525,12 +662,6 @@ dependencies = [ "digest 0.10.7", ] -[[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" @@ -580,12 +711,34 @@ dependencies = [ "piper", ] +[[package]] +name = "borsh" +version = "1.7.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2f3f6da4992df95bbcd9af42a6c7dcb994498fc9048230405f3b36ff7cd3f145" +dependencies = [ + "bytes", + "cfg_aliases", +] + +[[package]] +name = "built" +version = "0.8.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5c0e531d93d39c34eef561e929e8a7f86d77a5af08aac4f6d6e39976c51858e9" + [[package]] name = "bumpalo" version = "3.20.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "72f5acc6cb2ba439de613abc23857ec3d78374d8ed5ac84e9d11336e87da8649" +[[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.25.0" @@ -678,7 +831,7 @@ dependencies = [ "find-msvc-tools", "jobserver", "libc", - "shlex", + "shlex 2.0.1", ] [[package]] @@ -687,6 +840,15 @@ version = "1.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "6d43a04d8753f35258c91f8ec639f792891f748a1edbd759cf1dcea3382ad83c" +[[package]] +name = "cexpr" +version = "0.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6fac387a98bb7c37292057cffc56d62ecb629900026402633ae9160df93a8766" +dependencies = [ + "nom 7.1.3", +] + [[package]] name = "cfg-if" version = "1.0.4" @@ -743,6 +905,19 @@ dependencies = [ "zeroize", ] +[[package]] +name = "chrono" +version = "0.4.45" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1aa79e62e7697b8e29b513a68abacf485adcd1fe8284a4316c5ae868e6633327" +dependencies = [ + "iana-time-zone", + "js-sys", + "num-traits", + "wasm-bindgen", + "windows-link", +] + [[package]] name = "cipher" version = "0.4.4" @@ -754,6 +929,17 @@ dependencies = [ "zeroize", ] +[[package]] +name = "clang-sys" +version = "1.8.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0b023947811758c97c59bf9d1c188fd619ad4718dcaa767947df1cadb14f39f4" +dependencies = [ + "glob", + "libc", + "libloading", +] + [[package]] name = "clipboard-win" version = "5.4.1" @@ -763,6 +949,15 @@ dependencies = [ "error-code", ] +[[package]] +name = "clru" +version = "0.6.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "197fd99cb113a8d5d9b6376f3aa817f32c1078f2343b714fff7d2ca44fdf67d5" +dependencies = [ + "hashbrown 0.16.1", +] + [[package]] name = "cmake" version = "0.1.58" @@ -779,15 +974,10 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "0c9ea0ac24bc397ab3c98583a3c9ba74fa56b09a4449bbe172b9b1ddb016027a" [[package]] -name = "codespan-reporting" -version = "0.12.0" +name = "color_quant" +version = "1.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "fe6d2e5af09e8c8ad56c969f2157a3d4238cebc7c55f0a517728c38f7b200f81" -dependencies = [ - "serde", - "termcolor", - "unicode-width", -] +checksum = "3d7b894f5411737b7867f4827955924d7c254fc9f4d91a6aad6b097804b1018b" [[package]] name = "combine" @@ -808,6 +998,27 @@ dependencies = [ "crossbeam-utils", ] +[[package]] +name = "const-field-offset" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9713489fbc45a2d113f58d17448c73115d533ed27fd2f2d0e5e5c69663eaf19b" +dependencies = [ + "const-field-offset-macro", + "field-offset", +] + +[[package]] +name = "const-field-offset-macro" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9b5dc851b7ea80cc4f69aa28632385ffd3e0f88014d53db3f2e5a781896ca783" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + [[package]] name = "const-oid" version = "0.9.6" @@ -820,6 +1031,29 @@ version = "0.10.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "a6ef517f0926dd24a1582492c791b6a4818a4d94e789a334894aa15b0d12f55c" +[[package]] +name = "convert_case" +version = "0.10.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "633458d4ef8c78b72454de2d54fd6ab2e60f9e02be22f3c6104cdc8a4e0fceb9" +dependencies = [ + "unicode-segmentation", +] + +[[package]] +name = "copypasta" +version = "0.10.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3e6811e17f81fe246ef2bc553f76b6ee6ab41a694845df1d37e52a92b7bbd38a" +dependencies = [ + "clipboard-win", + "objc2 0.5.2", + "objc2-app-kit 0.2.2", + "objc2-foundation 0.2.2", + "smithay-clipboard", + "x11-clipboard", +] + [[package]] name = "core-foundation" version = "0.9.4" @@ -854,7 +1088,7 @@ checksum = "c07782be35f9e1140080c6b96f0d44b739e2278479f64e02fdab4e32dfd8b081" dependencies = [ "bitflags 1.3.2", "core-foundation 0.9.4", - "core-graphics-types 0.1.3", + "core-graphics-types", "foreign-types", "libc", ] @@ -871,14 +1105,12 @@ dependencies = [ ] [[package]] -name = "core-graphics-types" -version = "0.2.0" +name = "core_maths" +version = "0.1.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3d44a101f213f6c4cdc1853d4b78aef6db6bdfa3468798cc1d9912f4735013eb" +checksum = "77745e017f5edba1a9c1d854f6f3a52dac8a12dd5af5d2f54aecf61e43d80d30" dependencies = [ - "bitflags 2.13.0", - "core-foundation 0.10.1", - "libc", + "libm", ] [[package]] @@ -895,6 +1127,12 @@ dependencies = [ "objc2-core-foundation", ] +[[package]] +name = "countme" +version = "3.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7704b5fdd17b18ae31c4c1da5a2e0305a2bf17b5249300a9ee9ed7b72114c636" + [[package]] name = "cpal" version = "0.17.3" @@ -922,7 +1160,7 @@ dependencies = [ "wasm-bindgen", "wasm-bindgen-futures", "web-sys", - "windows 0.62.2", + "windows", ] [[package]] @@ -951,8 +1189,8 @@ dependencies = [ "objc2-core-foundation", "objc2-foundation 0.3.2", "web-sys", - "windows 0.62.2", - "windows-core 0.62.2", + "windows", + "windows-core", ] [[package]] @@ -982,6 +1220,40 @@ dependencies = [ "cfg-if", ] +[[package]] +name = "critical-section" +version = "1.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "790eea4361631c5e7d22598ecd5723ff611904e3344ce8720784c93e3d83d40b" + +[[package]] +name = "crossbeam-channel" +version = "0.5.16" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d85363c37faeca707aef026efa9f3b34d077bce547e48f770770625c6013679e" +dependencies = [ + "crossbeam-utils", +] + +[[package]] +name = "crossbeam-deque" +version = "0.8.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5181e0de7b61eb03a81e347d6dd8797bae9da5146707b51077e2d71a54ec0ceb" +dependencies = [ + "crossbeam-epoch", + "crossbeam-utils", +] + +[[package]] +name = "crossbeam-epoch" +version = "0.9.20" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2d6914041f254d6e9176c01941b21115dcfb7089e55135a35411081bd106ef3f" +dependencies = [ + "crossbeam-utils", +] + [[package]] name = "crossbeam-utils" version = "0.8.22" @@ -1014,6 +1286,15 @@ dependencies = [ "hybrid-array", ] +[[package]] +name = "ctor" +version = "0.10.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "83cf0d42651b16c6dfe68685716d18480d18a9c39c62d76e8cf3eb6ed5d8bcbf" +dependencies = [ + "dtor", +] + [[package]] name = "ctutils" version = "0.4.2" @@ -1062,6 +1343,12 @@ version = "0.11.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "0c87e182de0887fd5361989c677c4e8f5000cd9491d6d563161a8f3a5519fc7f" +[[package]] +name = "data-url" +version = "0.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "be1e0bca6c3637f992fc1cc7cbc52a78c1ef6db076dbf1059c4323d6a2048376" + [[package]] name = "der" version = "0.7.10" @@ -1072,6 +1359,40 @@ dependencies = [ "zeroize", ] +[[package]] +name = "derive_more" +version = "2.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d751e9e49156b02b44f9c1815bcb94b984cdcc4396ecc32521c739452808b134" +dependencies = [ + "derive_more-impl", +] + +[[package]] +name = "derive_more-impl" +version = "2.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "799a97264921d8623a957f6c3b9011f3b5492f557bbb7a5a19b7fa6d06ba8dcb" +dependencies = [ + "convert_case", + "proc-macro2", + "quote", + "rustc_version", + "syn", + "unicode-xid", +] + +[[package]] +name = "derive_utils" +version = "0.15.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "362f47930db19fe7735f527e6595e4900316b893ebf6d48ad3d31be928d57dd6" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + [[package]] name = "digest" version = "0.10.7" @@ -1152,15 +1473,6 @@ dependencies = [ "libloading", ] -[[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" @@ -1173,6 +1485,52 @@ version = "0.1.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d8b14ccef22fc6f5a8f4d7d768562a182c04ce9a3b3157b91390b52ddfdf1a76" +[[package]] +name = "drm" +version = "0.14.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "80bc8c5c6c2941f70a55c15f8d9f00f9710ebda3ffda98075f996a0e6c92756f" +dependencies = [ + "bitflags 2.13.0", + "bytemuck", + "drm-ffi", + "drm-fourcc", + "libc", + "rustix 0.38.44", +] + +[[package]] +name = "drm-ffi" +version = "0.9.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "51a91c9b32ac4e8105dec255e849e0d66e27d7c34d184364fb93e469db08f690" +dependencies = [ + "drm-sys", + "rustix 1.1.4", +] + +[[package]] +name = "drm-fourcc" +version = "2.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0aafbcdb8afc29c1a7ee5fbe53b5d62f4565b35a042a662ca9fecd0b54dae6f4" + +[[package]] +name = "drm-sys" +version = "0.8.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ecc8e1361066d91f5ffccff060a3c3be9c3ecde15be2959c1937595f7a82a9f8" +dependencies = [ + "libc", + "linux-raw-sys 0.9.4", +] + +[[package]] +name = "dtor" +version = "0.8.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "edf234dd1594d6dd434a8fb8cada51ddbbc593e40e4a01556a0b31c62da2775b" + [[package]] name = "easyfft" version = "0.4.2" @@ -1186,16 +1544,6 @@ dependencies = [ "rustfft", ] -[[package]] -name = "ecolor" -version = "0.33.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "71ddb8ac7643d1dba1bb02110e804406dd459a838efcb14011ced10556711a8e" -dependencies = [ - "bytemuck", - "emath", -] - [[package]] name = "ed25519" version = "2.2.3" @@ -1222,127 +1570,10 @@ dependencies = [ ] [[package]] -name = "eframe" -version = "0.33.3" +name = "either" +version = "1.16.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "457481173e6db5ca9fa2be93a58df8f4c7be639587aeb4853b526c6cf87db4e6" -dependencies = [ - "ahash", - "bytemuck", - "document-features", - "egui", - "egui-wgpu", - "egui-winit", - "egui_glow", - "glow", - "glutin", - "glutin-winit", - "image", - "js-sys", - "log", - "objc2 0.5.2", - "objc2-app-kit 0.2.2", - "objc2-foundation 0.2.2", - "parking_lot", - "percent-encoding", - "pollster", - "profiling", - "raw-window-handle", - "static_assertions", - "wasm-bindgen", - "wasm-bindgen-futures", - "web-sys", - "web-time", - "wgpu", - "windows-sys 0.61.2", - "winit", -] - -[[package]] -name = "egui" -version = "0.33.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6a9b567d356674e9a5121ed3fedfb0a7c31e059fe71f6972b691bcd0bfc284e3" -dependencies = [ - "accesskit", - "ahash", - "bitflags 2.13.0", - "emath", - "epaint", - "log", - "nohash-hasher", - "profiling", - "smallvec", - "unicode-segmentation", -] - -[[package]] -name = "egui-wgpu" -version = "0.33.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5e4d209971c84b2352a06174abdba701af1e552ce56b144d96f2bd50a3c91236" -dependencies = [ - "ahash", - "bytemuck", - "document-features", - "egui", - "epaint", - "log", - "profiling", - "thiserror 2.0.18", - "type-map", - "web-time", - "wgpu", - "winit", -] - -[[package]] -name = "egui-winit" -version = "0.33.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ec6687e5bb551702f4ad10ac428bab12acf9d53047ebb1082d4a0ed8c6251a29" -dependencies = [ - "accesskit_winit", - "arboard", - "bytemuck", - "egui", - "log", - "objc2 0.5.2", - "objc2-foundation 0.2.2", - "objc2-ui-kit", - "profiling", - "raw-window-handle", - "smithay-clipboard", - "web-time", - "webbrowser", - "winit", -] - -[[package]] -name = "egui_glow" -version = "0.33.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6420863ea1d90e750f75075231a260030ad8a9f30a7cef82cdc966492dc4c4eb" -dependencies = [ - "bytemuck", - "egui", - "glow", - "log", - "memoffset", - "profiling", - "wasm-bindgen", - "web-sys", - "winit", -] - -[[package]] -name = "emath" -version = "0.33.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "491bdf728bf25ddd9ad60d4cf1c48588fa82c013a2440b91aa7fc43e34a07c32" -dependencies = [ - "bytemuck", -] +checksum = "91622ff5e7162018101f2fea40d6ebf4a78bbe5a49736a2020649edf9693679e" [[package]] name = "encoding_rs" @@ -1381,28 +1612,24 @@ dependencies = [ ] [[package]] -name = "epaint" -version = "0.33.3" +name = "equator" +version = "0.4.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "009d0dd3c2163823a0abdb899451ecbc78798dec545ee91b43aff1fa790bab62" +checksum = "4711b213838dfee0117e3be6ac926007d7f433d7bbe33595975d4190cb07e6fc" dependencies = [ - "ab_glyph", - "ahash", - "bytemuck", - "ecolor", - "emath", - "epaint_default_fonts", - "log", - "nohash-hasher", - "parking_lot", - "profiling", + "equator-macro", ] [[package]] -name = "epaint_default_fonts" -version = "0.33.3" +name = "equator-macro" +version = "0.4.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5c4fbe202b6578d3d56428fa185cdf114a05e49da05f477b3c7f0fbb221f1862" +checksum = "44f23cf4b44bfce11a86ace86f8a73ffdec849c9fd00a386a53d278bd9e81fb3" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] [[package]] name = "equivalent" @@ -1426,6 +1653,15 @@ version = "3.3.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "dea2df4cf52843e0452895c455a1a2cfbb842a1e7329671acf418fdc53ed4c59" +[[package]] +name = "euclid" +version = "0.22.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f1a05365e3b1c6d1650318537c7460c6923f1abdd272ad6842baa2b509957a06" +dependencies = [ + "num-traits", +] + [[package]] name = "event-listener" version = "5.4.1" @@ -1447,6 +1683,23 @@ dependencies = [ "pin-project-lite", ] +[[package]] +name = "exr" +version = "1.74.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "711fe42c9964295e01ee3fba3f9fe0e1d24b98886950d68efe81b1c76e21adf3" +dependencies = [ + "bit_field", + "half", + "lebe", + "miniz_oxide", + "num-complex", + "pulp", + "rayon-core", + "smallvec", + "zune-inflate", +] + [[package]] name = "extended" version = "0.1.0" @@ -1474,18 +1727,69 @@ dependencies = [ "simd-adler32", ] +[[package]] +name = "femtovg" +version = "0.25.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f43d05da42e81724c16d34a150fb7fda53d3f786e5a94673ee526ff8602f0f6a" +dependencies = [ + "bitflags 2.13.0", + "bytemuck", + "fnv", + "glow", + "imgref", + "itertools 0.14.0", + "log", + "rgb", + "slotmap", + "swash", + "wasm-bindgen", + "web-sys", +] + [[package]] name = "fiat-crypto" version = "0.2.9" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "28dea519a9695b9977216879a3ebfddf92f1c08c05d984f8996aecd6ecdc811d" +[[package]] +name = "field-offset" +version = "0.3.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "38e2275cc4e4fc009b0669731a1e5ab7ebf11f469eaede2bab9309a5b4d6057f" +dependencies = [ + "memoffset", + "rustc_version", +] + +[[package]] +name = "filetime" +version = "0.2.29" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5c287a33c7f0a620c38e641e7f60827713987b3c0f26e8ddc9462cc69cf75759" +dependencies = [ + "cfg-if", + "libc", +] + [[package]] name = "find-msvc-tools" version = "0.1.9" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "5baebc0774151f905a1a2cc41989300b1e6fbb29aff0ceffa1064fdd3088d582" +[[package]] +name = "fixed_decimal" +version = "0.7.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "79c3c892f121fff406e5dd6b28c1b30096b95111c30701a899d4f2b18da6d1bd" +dependencies = [ + "displaydoc", + "smallvec", + "writeable", +] + [[package]] name = "flate2" version = "1.1.9" @@ -1497,10 +1801,16 @@ dependencies = [ ] [[package]] -name = "foldhash" -version = "0.1.5" +name = "float-cmp" +version = "0.9.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d9c4f5dac5e15c24eb999c26181a6ca40b39fe946cbe4c263c7209467bc83af2" +checksum = "98de4bbd547a563b716d8dfa9aad1cb19bfab00f4fa09a6a4ed21dbcf44ce9c4" + +[[package]] +name = "fnv" +version = "1.0.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3f9eec918d3f24069decb9af1554cad7c880e2da24a9afd88aca000531ab82c1" [[package]] name = "foldhash" @@ -1508,6 +1818,49 @@ version = "0.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "77ce24cb58228fbb8aa041425bb1050850ac19177686ea6e0f41a70416f56fdb" +[[package]] +name = "font-types" +version = "0.11.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5b38ad915f6dadd993ced50848a8291a543bd41ca62bc10740d5e64e2ab4cfd7" +dependencies = [ + "bytemuck", +] + +[[package]] +name = "fontdb" +version = "0.23.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "457e789b3d1202543297a350643cf459f836cade38934e7a4cf6a39e7cde2905" +dependencies = [ + "log", + "slotmap", + "tinyvec", + "ttf-parser", +] + +[[package]] +name = "fontique" +version = "0.10.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "274fa4f0f0a926ae182c7c076c078cce8a38471d15e61a102a02cac984be9813" +dependencies = [ + "hashbrown 0.17.1", + "linebender_resource_handle", + "memmap2", + "objc2 0.6.4", + "objc2-core-foundation", + "objc2-core-text", + "objc2-foundation 0.3.2", + "parlance", + "read-fonts", + "roxmltree", + "smallvec", + "windows", + "windows-core", + "yeslogic-fontconfig-sys", +] + [[package]] name = "foreign-types" version = "0.5.0" @@ -1544,12 +1897,48 @@ dependencies = [ "percent-encoding", ] +[[package]] +name = "futures" +version = "0.3.32" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8b147ee9d1f6d097cef9ce628cd2ee62288d963e16fb287bd9286455b241382d" +dependencies = [ + "futures-channel", + "futures-core", + "futures-executor", + "futures-io", + "futures-sink", + "futures-task", + "futures-util", +] + +[[package]] +name = "futures-channel" +version = "0.3.32" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "07bbe89c50d7a535e539b8c17bc0b49bdb77747034daa8087407d655f3f7cc1d" +dependencies = [ + "futures-core", + "futures-sink", +] + [[package]] name = "futures-core" version = "0.3.32" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "7e3450815272ef58cec6d564423f6e755e25379b217b0bc688e295ba24df6b1d" +[[package]] +name = "futures-executor" +version = "0.3.32" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "baf29c38818342a3b26b5b923639e7b1f4a61fc5e76102d4b1981c6dc7a7579d" +dependencies = [ + "futures-core", + "futures-task", + "futures-util", +] + [[package]] name = "futures-io" version = "0.3.32" @@ -1580,6 +1969,12 @@ dependencies = [ "syn", ] +[[package]] +name = "futures-sink" +version = "0.3.32" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c39754e157331b013978ec91992bde1ac089843443c49cbc7f46150b0fad0893" + [[package]] name = "futures-task" version = "0.3.32" @@ -1592,13 +1987,39 @@ version = "0.3.32" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "389ca41296e6190b48053de0321d02a77f32f8a5d2461dd38762c0593805c6d6" dependencies = [ + "futures-channel", "futures-core", + "futures-io", "futures-macro", + "futures-sink", "futures-task", + "memchr", "pin-project-lite", "slab", ] +[[package]] +name = "gbm" +version = "0.18.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ce852e998d3ca5e4a97014fb31c940dc5ef344ec7d364984525fd11e8a547e6a" +dependencies = [ + "bitflags 2.13.0", + "drm", + "drm-fourcc", + "gbm-sys", + "libc", +] + +[[package]] +name = "gbm-sys" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c13a5f2acc785d8fb6bf6b7ab6bfb0ef5dad4f4d97e8e70bb8e470722312f76f" +dependencies = [ + "libc", +] + [[package]] name = "generic-array" version = "0.14.7" @@ -1626,7 +2047,16 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "1bd49230192a3797a9a4d6abe9b3eed6f7fa4c8a8a4947977c6f80025f92cbd8" dependencies = [ "rustix 1.1.4", - "windows-link 0.2.1", + "windows-link", +] + +[[package]] +name = "getopts" +version = "0.2.24" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cfe4fbac503b8d1f88e6676011885f34b7174f46e59956bba534ba83abded4df" +dependencies = [ + "unicode-width", ] [[package]] @@ -1664,6 +2094,16 @@ dependencies = [ "rand_core 0.10.1", ] +[[package]] +name = "gif" +version = "0.14.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ee8cfcc411d9adbbaba82fb72661cc1bcca13e8bba98b364e62b2dba8f960159" +dependencies = [ + "color_quant", + "weezl", +] + [[package]] name = "gl_generator" version = "0.14.0" @@ -1676,10 +2116,16 @@ dependencies = [ ] [[package]] -name = "glow" -version = "0.16.0" +name = "glob" +version = "0.3.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c5e5ea60d70410161c8bf5da3fdfeaa1c72ed2c15f8bbb9d19fe3a4fad085f08" +checksum = "0cc23270f6e1808e30a928bdc84dea0b9b4136a8bc82338574f23baf47bbd280" + +[[package]] +name = "glow" +version = "0.17.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "29038e1c483364cc6bb3cf78feee1816002e127c331a1eec55a4d202b9e1adb5" dependencies = [ "js-sys", "slotmap", @@ -1754,55 +2200,10 @@ dependencies = [ ] [[package]] -name = "gpu-alloc" -version = "0.6.2" +name = "grid" +version = "1.0.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "45cf04b2726f02df5508c6de726acdc90cdf97ac771a9a0ffd8ba10a6e696bf9" -dependencies = [ - "bitflags 2.13.0", - "gpu-alloc-types", -] - -[[package]] -name = "gpu-alloc-types" -version = "0.3.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b2bbed164dd10ed526c2e4fe3e721ca4a71c61730e5aafac6844b417b3227058" -dependencies = [ - "bitflags 2.13.0", -] - -[[package]] -name = "gpu-allocator" -version = "0.27.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c151a2a5ef800297b4e79efa4f4bec035c5f51d5ae587287c9b952bdf734cacd" -dependencies = [ - "log", - "presser", - "thiserror 1.0.69", - "windows 0.58.0", -] - -[[package]] -name = "gpu-descriptor" -version = "0.3.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b89c83349105e3732062a895becfc71a8f921bb71ecbbdd8ff99263e3b53a0ca" -dependencies = [ - "bitflags 2.13.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.13.0", -] +checksum = "b40ca9252762c466af32d0b1002e91e4e1bc5398f77455e55474deb466355ff5" [[package]] name = "half" @@ -1812,26 +2213,36 @@ checksum = "6ea2d84b969582b4b1864a92dc5d27cd2b77b622a8d79306834f1be5ba20d84b" dependencies = [ "cfg-if", "crunchy", - "num-traits", "zerocopy", ] [[package]] -name = "hashbrown" -version = "0.15.5" +name = "harfrust" +version = "0.8.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9229cfe53dfd69f0609a49f65461bd93001ea1ef889cd5529dd176593f5338a1" +checksum = "d12c7c642d4ce8c2e784b4751a6634bd89583912265add4a679a8882d123fbcd" dependencies = [ - "foldhash 0.1.5", + "bitflags 2.13.0", + "bytemuck", + "read-fonts", + "smallvec", ] +[[package]] +name = "hashbrown" +version = "0.14.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e5274423e17b7c9fc20b6e7e208532f9b19825d82dfd615708b70edd83df41f1" + [[package]] name = "hashbrown" version = "0.16.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "841d1cc9bed7f9236f321df977030373f4a4163ae1a7dbfe1a51a2c1a51d9100" dependencies = [ - "foldhash 0.2.0", + "allocator-api2", + "equivalent", + "foldhash", ] [[package]] @@ -1839,6 +2250,21 @@ name = "hashbrown" version = "0.17.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ed5909b6e89a2db4456e54cd5f673791d7eca6732202bbf2a9cc504fe2f9b84a" +dependencies = [ + "foldhash", +] + +[[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" @@ -1852,12 +2278,6 @@ 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 = "hkdf" version = "0.13.0" @@ -1876,6 +2296,12 @@ dependencies = [ "digest 0.11.3", ] +[[package]] +name = "htmlparser" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "48ce8546b993eaf241d69ded33b1be6d205dd9857ec879d9d18bd05d3676e144" + [[package]] name = "hybrid-array" version = "0.4.13" @@ -1885,6 +2311,323 @@ dependencies = [ "typenum", ] +[[package]] +name = "i-slint-backend-linuxkms" +version = "1.17.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "68e76f2b13846ca54bba6b8eb3b5454b26879ad987396d0a2d8abedc31a6c469" +dependencies = [ + "bytemuck", + "calloop 0.14.4", + "cfg_aliases", + "drm", + "gbm", + "glutin", + "i-slint-common", + "i-slint-core", + "i-slint-renderer-femtovg", + "i-slint-renderer-software", + "input", + "memmap2", + "nix", + "raw-window-handle", + "xkbcommon", +] + +[[package]] +name = "i-slint-backend-selector" +version = "1.17.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2247a4590a02a466b2f4916cc789dfe66247f6af826f00eb84a2685259a45c1c" +dependencies = [ + "cfg-if", + "cfg_aliases", + "i-slint-backend-linuxkms", + "i-slint-backend-testing", + "i-slint-backend-winit", + "i-slint-common", + "i-slint-core", + "i-slint-core-macros", + "i-slint-renderer-femtovg", +] + +[[package]] +name = "i-slint-backend-testing" +version = "1.17.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "521e901e3d47ab829c0ef500c63155776208707cd93259e6a7803ed627fa2786" +dependencies = [ + "cfg_aliases", + "i-slint-common", + "i-slint-core", + "i-slint-renderer-software", + "vtable", +] + +[[package]] +name = "i-slint-backend-winit" +version = "1.17.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a27c7e8f53418d3017362465367b6612b6e439a68097d627d1b91c94b885f0a6" +dependencies = [ + "accesskit", + "accesskit_winit", + "block2 0.6.2", + "bytemuck", + "cfg-if", + "cfg_aliases", + "copypasta", + "derive_more", + "futures", + "glutin", + "glutin-winit", + "i-slint-common", + "i-slint-core", + "i-slint-core-macros", + "i-slint-renderer-femtovg", + "i-slint-renderer-skia", + "i-slint-renderer-software", + "imgref", + "lyon_path", + "muda", + "objc2 0.6.4", + "objc2-app-kit 0.3.2", + "objc2-foundation 0.3.2", + "objc2-quartz-core 0.3.2", + "objc2-ui-kit 0.3.2", + "pin-weak", + "raw-window-handle", + "rgb", + "scoped-tls-hkt", + "scopeguard", + "softbuffer", + "strum", + "vtable", + "wasm-bindgen", + "web-sys", + "webbrowser", + "windows", + "winit", + "zbus", +] + +[[package]] +name = "i-slint-common" +version = "1.17.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "52e63f8ea55ba21e44567ae67d2b01a4f5d09bb1fab693b1e6c79d96bef9e456" +dependencies = [ + "derive_more", + "fontique", + "htmlparser", + "icu_decimal", + "icu_locale_core", + "icu_provider", + "pulldown-cmark", + "resvg", + "skrifa", +] + +[[package]] +name = "i-slint-compiler" +version = "1.17.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "45ea275b15a425c7f2f77481151e1f5f8f1ea83feae580273090ef6b9e192218" +dependencies = [ + "annotate-snippets", + "by_address", + "data-url", + "derive_more", + "i-slint-common", + "icu_normalizer", + "image", + "itertools 0.14.0", + "linked_hash_set", + "lyon_extra", + "lyon_path", + "num_enum", + "proc-macro2", + "quote", + "rayon", + "resvg", + "rowan", + "rspolib", + "skrifa", + "smol_str 0.3.6", + "strum", + "swash", + "typed-index-collections", + "unicode-segmentation", + "url", +] + +[[package]] +name = "i-slint-core" +version = "1.17.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4e407dc2bd5385dcbdd9c1cf927213d2e4bacda667545abf8839c60f8290f754" +dependencies = [ + "async-channel", + "auto_enums", + "bitflags 2.13.0", + "cfg-if", + "chrono", + "clru", + "const-field-offset", + "derive_more", + "euclid", + "i-slint-common", + "i-slint-core-macros", + "icu_normalizer", + "image", + "ksni", + "lyon_algorithms", + "lyon_extra", + "lyon_geom", + "lyon_path", + "num-traits", + "objc2 0.6.4", + "objc2-app-kit 0.3.2", + "objc2-foundation 0.3.2", + "once_cell", + "parley", + "pin-project", + "pin-weak", + "portable-atomic", + "raw-window-handle", + "resvg", + "rgb", + "scopeguard", + "skrifa", + "slab", + "strum", + "swash", + "sys-locale", + "taffy", + "unicode-linebreak", + "unicode-script", + "unicode-segmentation", + "vtable", + "wasm-bindgen", + "web-sys", + "web-time", + "windows", +] + +[[package]] +name = "i-slint-core-macros" +version = "1.17.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3a842640da4c4d0909d25cc5a51e1f9cde02bd48f7fdfa94df10d595d4e3c80a" +dependencies = [ + "quote", + "serde_json", + "syn", +] + +[[package]] +name = "i-slint-renderer-femtovg" +version = "1.17.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "78d0d99a18d66738b684758fe8460743a0aa0f64a96e05940942231a340085b1" +dependencies = [ + "cfg-if", + "const-field-offset", + "derive_more", + "femtovg", + "glow", + "i-slint-common", + "i-slint-core", + "i-slint-core-macros", + "imgref", + "lyon_path", + "pin-weak", + "rgb", + "wasm-bindgen", + "web-sys", +] + +[[package]] +name = "i-slint-renderer-skia" +version = "1.17.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7b6eed7f3f0a9a3d3ca6e8b9d4ca233371d989351fdb2a7ab88ec368b99e7b57" +dependencies = [ + "bytemuck", + "cfg-if", + "cfg_aliases", + "clru", + "const-field-offset", + "derive_more", + "glow", + "glutin", + "i-slint-common", + "i-slint-core", + "i-slint-core-macros", + "lyon_path", + "objc2 0.6.4", + "objc2-app-kit 0.3.2", + "objc2-core-foundation", + "objc2-foundation 0.3.2", + "objc2-metal 0.3.2", + "objc2-quartz-core 0.3.2", + "pin-weak", + "raw-window-handle", + "raw-window-metal", + "read-fonts", + "scoped-tls-hkt", + "skia-safe", + "softbuffer", + "unicode-segmentation", + "vtable", + "windows", + "write-fonts", +] + +[[package]] +name = "i-slint-renderer-software" +version = "1.17.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "73496f164f8e471877c35e0fadb996dcc674922712abd9c2be2cbe80c58859bd" +dependencies = [ + "bytemuck", + "clru", + "derive_more", + "euclid", + "i-slint-common", + "i-slint-core", + "integer-sqrt", + "lyon_path", + "num-traits", + "skrifa", + "swash", + "zeno", +] + +[[package]] +name = "iana-time-zone" +version = "0.1.65" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e31bc9ad994ba00e440a8aa5c9ef0ec67d5cb5e5cb0cc7f8b744a35b389cc470" +dependencies = [ + "android_system_properties", + "core-foundation-sys", + "iana-time-zone-haiku", + "js-sys", + "log", + "wasm-bindgen", + "windows-core", +] + +[[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 = "icu_collections" version = "2.2.0" @@ -1899,6 +2642,44 @@ dependencies = [ "zerovec", ] +[[package]] +name = "icu_decimal" +version = "2.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "288247df2e32aa776ac54fdd64de552149ac43cb840f2761811f0e8d09719dd4" +dependencies = [ + "displaydoc", + "fixed_decimal", + "icu_decimal_data", + "icu_locale", + "icu_locale_core", + "icu_plurals", + "icu_provider", + "writeable", + "zerovec", +] + +[[package]] +name = "icu_decimal_data" +version = "2.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6f14a5ca9e8af29eef62064f269078424283d90dbaffeac5225addf62aaabc22" + +[[package]] +name = "icu_locale" +version = "2.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d5a396343c7208121dc86e35623d3dfe19814a7613cfd14964994cdc9c9a2e26" +dependencies = [ + "icu_collections", + "icu_locale_core", + "icu_locale_data", + "icu_provider", + "potential_utf", + "tinystr", + "zerovec", +] + [[package]] name = "icu_locale_core" version = "2.2.0" @@ -1907,11 +2688,18 @@ checksum = "92219b62b3e2b4d88ac5119f8904c10f8f61bf7e95b640d25ba3075e6cac2c29" dependencies = [ "displaydoc", "litemap", + "serde", "tinystr", "writeable", "zerovec", ] +[[package]] +name = "icu_locale_data" +version = "2.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d5fdcc9ac77c6d74ff5cf6e65ef3181d6af32003b16fce3a77fb451d2f695993" + [[package]] name = "icu_normalizer" version = "2.2.0" @@ -1932,6 +2720,25 @@ version = "2.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "da3be0ae77ea334f4da67c12f149704f19f81d1adf7c51cf482943e84a2bad38" +[[package]] +name = "icu_plurals" +version = "2.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2a50023f1d49ad5c4333380328a0d4a19e4b9d6d842ec06639affd5ba47c8103" +dependencies = [ + "fixed_decimal", + "icu_locale", + "icu_plurals_data", + "icu_provider", + "zerovec", +] + +[[package]] +name = "icu_plurals_data" +version = "2.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8485497155dc865f901decb93ecc20d3e467df67bfeceb91e3ba34e2b11e8e1d" + [[package]] name = "icu_properties" version = "2.2.0" @@ -1960,6 +2767,8 @@ checksum = "139c4cf31c8b5f33d7e199446eff9c1e02decfc2f0eec2c8d71f65befa45b421" dependencies = [ "displaydoc", "icu_locale_core", + "serde", + "stable_deref_trait", "writeable", "yoke", "zerofrom", @@ -1967,6 +2776,27 @@ dependencies = [ "zerovec", ] +[[package]] +name = "icu_segmenter" +version = "2.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5c0794db0b1a86193ac9c48768d0e6c52c54448e0870ad87907d456ee0dac964" +dependencies = [ + "icu_collections", + "icu_locale", + "icu_provider", + "icu_segmenter_data", + "potential_utf", + "utf8_iter", + "zerovec", +] + +[[package]] +name = "icu_segmenter_data" +version = "2.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e4a2c462a4d927d512f5f882a033ddd62f33a05bb9f230d98f736ac3dc85938f" + [[package]] name = "idna" version = "1.1.0" @@ -1996,12 +2826,44 @@ checksum = "85ab80394333c02fe689eaf900ab500fbd0c2213da414687ebf995a65d5a6104" dependencies = [ "bytemuck", "byteorder-lite", + "color_quant", + "exr", + "gif", + "image-webp", "moxcms", "num-traits", "png", + "qoi", + "ravif", + "rayon", + "rgb", "tiff", + "zune-core", + "zune-jpeg", ] +[[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.14.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "09e54e57b4c48b40f7aec75635392b12b3421fa26fe8b4332e63138ed278459c" + +[[package]] +name = "imgref" +version = "1.12.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "89194689a993ab15268672e99e7b0e19da2da3268ac682e8f02d29d4d1434cd7" + [[package]] name = "indexmap" version = "2.14.0" @@ -2021,6 +2883,74 @@ dependencies = [ "generic-array", ] +[[package]] +name = "input" +version = "0.10.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f9793345a65d71317763a33066b5d8351f8760dde8d4930fe9e39b5f14a7959d" +dependencies = [ + "bitflags 2.13.0", + "input-sys", + "libc", + "log", + "udev", +] + +[[package]] +name = "input-sys" +version = "1.19.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "36eee07d8e02bd95bf52b2e642cf13d33701b94c6e4b04fbf1d1fb07e9cb19e7" + +[[package]] +name = "integer-sqrt" +version = "0.1.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "276ec31bcb4a9ee45f58bec6f9ec700ae4cf4f4f8f2fa7e06cb406bd5ffdd770" +dependencies = [ + "num-traits", +] + +[[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", +] + +[[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 = "itertools" +version = "0.13.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "413ee7dfc52ee1a4949ceeb7dbc8a33f2d6c088194d9f922fb8318faf1f01186" +dependencies = [ + "either", +] + +[[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.18" @@ -2057,7 +2987,7 @@ dependencies = [ "simd_cesu8", "thiserror 2.0.18", "walkdir", - "windows-link 0.2.1", + "windows-link", ] [[package]] @@ -2123,14 +3053,14 @@ dependencies = [ ] [[package]] -name = "khronos-egl" -version = "6.0.0" +name = "keyboard-types" +version = "0.7.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6aae1df220ece3c0ada96b8153459b67eebe9ae9212258bb0134ae60416fdf76" +checksum = "b750dcadc39a09dbadd74e118f6dd6598df77fa01df0cfcdc52c28dece74528a" dependencies = [ - "libc", - "libloading", - "pkg-config", + "bitflags 2.13.0", + "serde", + "unicode-segmentation", ] [[package]] @@ -2139,18 +3069,63 @@ version = "3.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e2db585e1d738fc771bf08a151420d3ed193d9d895a36df7f6f8a9456b911ddc" +[[package]] +name = "ksni" +version = "0.3.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "da9eeb3f510b6148ae68f963af2c1fbb0de4d9e4e05f82813cfb319837c3ad2b" +dependencies = [ + "async-executor", + "async-io", + "async-lock", + "futures-channel", + "futures-lite", + "futures-util", + "pastey 0.2.3", + "serde", + "zbus", +] + +[[package]] +name = "kurbo" +version = "0.13.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4b60dfc32f652b926df6192e55525b16d186c69d47876c3ead4da5cc9f8450e2" +dependencies = [ + "arrayvec", + "euclid", + "polycool", + "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.186" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "68ab91017fe16c622486840e4c83c9a37afeff978bd239b5293d61ece587de66" +[[package]] +name = "libfuzzer-sys" +version = "0.4.13" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a9fd2f41a1cba099f79a0b6b6c35656cf7c03351a7bae8ff0f28f25270f929d2" +dependencies = [ + "arbitrary", + "cc", +] + [[package]] name = "libloading" version = "0.8.9" @@ -2158,7 +3133,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d7c4b02199fee7c5d21a5ae7d8cfa79a6ef5bb2fc834d6e9058e89c825efdc55" dependencies = [ "cfg-if", - "windows-link 0.2.1", + "windows-link", ] [[package]] @@ -2179,12 +3154,49 @@ dependencies = [ "redox_syscall 0.9.0", ] +[[package]] +name = "libudev-sys" +version = "0.1.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3c8469b4a23b962c1396b9b451dda50ef5b283e8dd309d69033475fa9b334324" +dependencies = [ + "libc", + "pkg-config", +] + +[[package]] +name = "linebender_resource_handle" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d4a5ff6bcca6c4867b1c4fd4ef63e4db7436ef363e0ad7531d1558856bae64f4" + +[[package]] +name = "linked-hash-map" +version = "0.5.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0717cef1bc8b636c6e1c1bbdefc09e6322da8a9321966e8928ef80d20f7f770f" + +[[package]] +name = "linked_hash_set" +version = "0.1.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "984fb35d06508d1e69fc91050cceba9c0b748f983e6739fa2c7a9237154c52c8" +dependencies = [ + "linked-hash-map", +] + [[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.9.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cd945864f07fe9f5371a27ad7b52a172b4b499999f1d97574c9fa68373937e12" + [[package]] name = "linux-raw-sys" version = "0.12.1" @@ -2197,12 +3209,6 @@ version = "0.8.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "92daf443525c4cce67b150400bc2316076100ce0b3686209eb8cf3c31612e6f0" -[[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" @@ -2218,6 +3224,56 @@ version = "0.4.33" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "0ceec5bc11778974d1bcb055b18002eba7f4b3518b6a0081b3af5f21666da9ad" +[[package]] +name = "loop9" +version = "0.1.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0fae87c125b03c1d2c0150c90365d7d6bcc53fb73a9acaef207d2d065860f062" +dependencies = [ + "imgref", +] + +[[package]] +name = "lyon_algorithms" +version = "1.0.20" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8575c0d003ae459399623c4def180c63b77f343b1a7fee64f249b349e7699a31" +dependencies = [ + "lyon_path", + "num-traits", +] + +[[package]] +name = "lyon_extra" +version = "1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7755f08423275157ad1680aaecc9ccb7e0cc633da3240fea2d1522935cc15c72" +dependencies = [ + "lyon_path", + "thiserror 2.0.18", +] + +[[package]] +name = "lyon_geom" +version = "1.0.19" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4336502e29e32af93cf2dad2214ed6003c17ceb5bd499df77b1de663b9042b92" +dependencies = [ + "arrayvec", + "euclid", + "num-traits", +] + +[[package]] +name = "lyon_path" +version = "1.0.19" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5c463f9c428b7fc5ec885dcd39ce4aa61e29111d0e33483f6f98c74e89d8621e" +dependencies = [ + "lyon_geom", + "num-traits", +] + [[package]] name = "mach2" version = "0.5.0" @@ -2233,15 +3289,6 @@ version = "0.6.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "dae608c151f68243f2b000364e1f7b186d9c29845f7d2d85bd31b9ad77ad552b" -[[package]] -name = "malloc_buf" -version = "0.0.6" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "62bb907fe88d54d8d9ce32a3cceab4218ed2f6b7d35617cafe9adf84e43919cb" -dependencies = [ - "libc", -] - [[package]] name = "matchers" version = "0.2.0" @@ -2251,6 +3298,16 @@ dependencies = [ "regex-automata", ] +[[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.8.3" @@ -2276,19 +3333,10 @@ dependencies = [ ] [[package]] -name = "metal" -version = "0.32.0" +name = "minimal-lexical" +version = "0.2.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "00c15a6f673ff72ddcc22394663290f870fb224c1bfce55734a75c414150e605" -dependencies = [ - "bitflags 2.13.0", - "block", - "core-graphics-types 0.2.0", - "foreign-types", - "log", - "objc", - "paste", -] +checksum = "68354c5c6bd36d73ff3feceb05efa59b6acb7626617f4962be322a825e61f79a" [[package]] name = "miniz_oxide" @@ -2322,31 +3370,30 @@ dependencies = [ ] [[package]] -name = "naga" -version = "27.0.3" +name = "muda" +version = "0.19.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "066cf25f0e8b11ee0df221219010f213ad429855f57c494f995590c861a9a7d8" +checksum = "1dd04e60bc0b07438a6771710ee1698f98f6ebbc7f89b61264af1563b8aeb878" dependencies = [ - "arrayvec", - "bit-set", - "bitflags 2.13.0", - "cfg-if", - "cfg_aliases", - "codespan-reporting", - "half", - "hashbrown 0.16.1", - "hexf-parse", - "indexmap", - "libm", - "log", - "num-traits", + "crossbeam-channel", + "dpi", + "keyboard-types", + "objc2 0.6.4", + "objc2-app-kit 0.3.2", + "objc2-core-foundation", + "objc2-foundation 0.3.2", "once_cell", - "rustc-hash 1.1.0", - "spirv", + "png", "thiserror 2.0.18", - "unicode-ident", + "windows-sys 0.61.2", ] +[[package]] +name = "natord" +version = "1.0.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "308d96db8debc727c3fd9744aac51751243420e46edf401010908da7f8d5e57c" + [[package]] name = "ndk" version = "0.9.0" @@ -2377,6 +3424,24 @@ dependencies = [ "jni-sys 0.3.1", ] +[[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.31.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cf20d2fde8ff38632c426f1165ed7436270b44f199fc55284c38276f9db47c3d" +dependencies = [ + "bitflags 2.13.0", + "cfg-if", + "cfg_aliases", + "libc", +] + [[package]] name = "nnnoiseless" version = "0.5.2" @@ -2388,10 +3453,38 @@ dependencies = [ ] [[package]] -name = "nohash-hasher" -version = "0.2.0" +name = "no_std_io2" +version = "0.9.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2bf50223579dc7cdcfb3bfcacf7069ff68243f8c363f62ffa99cf000a6b9c451" +checksum = "418abd1b6d34fbf6cae440dc874771b0525a604428704c76e48b29a5e67b8003" +dependencies = [ + "memchr", +] + +[[package]] +name = "nom" +version = "7.1.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d273983c5a657a70a3e8f2a01329822f3b8c8172b73826411a55751e404a0a4a" +dependencies = [ + "memchr", + "minimal-lexical", +] + +[[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 = "nu-ansi-term" @@ -2418,6 +3511,7 @@ version = "0.4.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "73f88a1307638156682bada9d7604135552957b7818057dcef22705b4d509495" dependencies = [ + "bytemuck", "num-traits", "serde", ] @@ -2485,15 +3579,6 @@ dependencies = [ "syn", ] -[[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-sys" version = "0.3.5" @@ -2529,10 +3614,10 @@ dependencies = [ "block2 0.5.1", "libc", "objc2 0.5.2", - "objc2-core-data", - "objc2-core-image", + "objc2-core-data 0.2.2", + "objc2-core-image 0.2.2", "objc2-foundation 0.2.2", - "objc2-quartz-core", + "objc2-quartz-core 0.2.2", ] [[package]] @@ -2542,10 +3627,18 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d49e936b501e5c5bf01fda3a9452ff86dc3ea98ad5f283e1455153142d97518c" dependencies = [ "bitflags 2.13.0", + "block2 0.6.2", + "libc", "objc2 0.6.4", + "objc2-cloud-kit 0.3.2", + "objc2-core-data 0.3.2", "objc2-core-foundation", "objc2-core-graphics", + "objc2-core-image 0.3.2", + "objc2-core-text", + "objc2-core-video", "objc2-foundation 0.3.2", + "objc2-quartz-core 0.3.2", ] [[package]] @@ -2587,6 +3680,17 @@ dependencies = [ "objc2-foundation 0.2.2", ] +[[package]] +name = "objc2-cloud-kit" +version = "0.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "73ad74d880bb43877038da939b7427bba67e9dd42004a18b809ba7d87cee241c" +dependencies = [ + "bitflags 2.13.0", + "objc2 0.6.4", + "objc2-foundation 0.3.2", +] + [[package]] name = "objc2-contacts" version = "0.2.2" @@ -2633,6 +3737,17 @@ dependencies = [ "objc2-foundation 0.2.2", ] +[[package]] +name = "objc2-core-data" +version = "0.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0b402a653efbb5e82ce4df10683b6b28027616a2715e90009947d50b8dd298fa" +dependencies = [ + "bitflags 2.13.0", + "objc2 0.6.4", + "objc2-foundation 0.3.2", +] + [[package]] name = "objc2-core-foundation" version = "0.3.2" @@ -2668,7 +3783,17 @@ dependencies = [ "block2 0.5.1", "objc2 0.5.2", "objc2-foundation 0.2.2", - "objc2-metal", + "objc2-metal 0.2.2", +] + +[[package]] +name = "objc2-core-image" +version = "0.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e5d563b38d2b97209f8e861173de434bd0214cf020e3423a52624cd1d989f006" +dependencies = [ + "objc2 0.6.4", + "objc2-foundation 0.3.2", ] [[package]] @@ -2683,6 +3808,31 @@ dependencies = [ "objc2-foundation 0.2.2", ] +[[package]] +name = "objc2-core-text" +version = "0.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0cde0dfb48d25d2b4862161a4d5fcc0e3c24367869ad306b0c9ec0073bfed92d" +dependencies = [ + "bitflags 2.13.0", + "objc2 0.6.4", + "objc2-core-foundation", + "objc2-core-graphics", +] + +[[package]] +name = "objc2-core-video" +version = "0.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d425caf1df73233f29fd8a5c3e5edbc30d2d4307870f802d18f00d83dc5141a6" +dependencies = [ + "bitflags 2.13.0", + "objc2 0.6.4", + "objc2-core-foundation", + "objc2-core-graphics", + "objc2-io-surface", +] + [[package]] name = "objc2-encode" version = "4.1.0" @@ -2750,6 +3900,17 @@ dependencies = [ "objc2-foundation 0.2.2", ] +[[package]] +name = "objc2-metal" +version = "0.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a0125f776a10d00af4152d74616409f0d4a2053a6f57fa5b7d6aa2854ac04794" +dependencies = [ + "bitflags 2.13.0", + "objc2 0.6.4", + "objc2-foundation 0.3.2", +] + [[package]] name = "objc2-quartz-core" version = "0.2.2" @@ -2760,7 +3921,20 @@ dependencies = [ "block2 0.5.1", "objc2 0.5.2", "objc2-foundation 0.2.2", - "objc2-metal", + "objc2-metal 0.2.2", +] + +[[package]] +name = "objc2-quartz-core" +version = "0.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "96c1358452b371bf9f104e21ec536d37a650eb10f7ee379fff67d2e08d537f1f" +dependencies = [ + "bitflags 2.13.0", + "objc2 0.6.4", + "objc2-core-foundation", + "objc2-foundation 0.3.2", + "objc2-metal 0.3.2", ] [[package]] @@ -2782,18 +3956,32 @@ dependencies = [ "bitflags 2.13.0", "block2 0.5.1", "objc2 0.5.2", - "objc2-cloud-kit", - "objc2-core-data", - "objc2-core-image", + "objc2-cloud-kit 0.2.2", + "objc2-core-data 0.2.2", + "objc2-core-image 0.2.2", "objc2-core-location", "objc2-foundation 0.2.2", "objc2-link-presentation", - "objc2-quartz-core", + "objc2-quartz-core 0.2.2", "objc2-symbols", "objc2-uniform-type-identifiers", "objc2-user-notifications", ] +[[package]] +name = "objc2-ui-kit" +version = "0.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d87d638e33c06f577498cbcc50491496a3ed4246998a7fbba7ccb98b1e7eab22" +dependencies = [ + "bitflags 2.13.0", + "block2 0.6.2", + "objc2 0.6.4", + "objc2-core-foundation", + "objc2-foundation 0.3.2", + "objc2-quartz-core 0.3.2", +] + [[package]] name = "objc2-uniform-type-identifiers" version = "0.2.2" @@ -2823,6 +4011,10 @@ name = "once_cell" version = "1.21.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9f7c3e4beb33f85d45ae3e3a1792185706c8e16d043238c593331cc7cd313b50" +dependencies = [ + "critical-section", + "portable-atomic", +] [[package]] name = "opaque-debug" @@ -2855,15 +4047,6 @@ dependencies = [ "libredox", ] -[[package]] -name = "ordered-float" -version = "5.3.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b7d950ca161dc355eaf28f82b11345ed76c6e1f6eb1f4f4479e0323b9e2fbd0e" -dependencies = [ - "num-traits", -] - [[package]] name = "ordered-stream" version = "0.2.0" @@ -2909,7 +4092,40 @@ dependencies = [ "libc", "redox_syscall 0.5.18", "smallvec", - "windows-link 0.2.1", + "windows-link", +] + +[[package]] +name = "parlance" +version = "0.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4b6937eda350acc1a5d05872c3cbf99fe78619c269096e2be3d4a350058639d5" + +[[package]] +name = "parley" +version = "0.10.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6b1cfcf399c774719fb1fa51bc6b91e86bdf003b03202a0902f1827ba6750746" +dependencies = [ + "fontique", + "harfrust", + "hashbrown 0.17.1", + "icu_normalizer", + "icu_properties", + "icu_segmenter", + "linebender_resource_handle", + "parlance", + "parley_data", + "skrifa", +] + +[[package]] +name = "parley_data" +version = "0.10.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d1d3755e2cc12c0625b0cd2f2773c6a37dd11d1531940c945ade4aeb78f2b145" +dependencies = [ + "icu_properties", ] [[package]] @@ -2929,12 +4145,73 @@ 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 = "pastey" +version = "0.2.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2ee67f1008b1ba2321834326597b8e186293b049a023cdef258527550b9935b4" + [[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.13.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c1562dc717473dbaa4c1f85a36410e03c047b2e7df7f45ee938fbef64ae7fadf" +dependencies = [ + "phf_macros", + "phf_shared", + "serde", +] + +[[package]] +name = "phf_generator" +version = "0.13.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "135ace3a761e564ec88c03a77317a7c6b80bb7f7135ef2544dbe054243b89737" +dependencies = [ + "fastrand", + "phf_shared", +] + +[[package]] +name = "phf_macros" +version = "0.13.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "812f032b54b1e759ccd5f8b6677695d5268c588701effba24601f6932f8269ef" +dependencies = [ + "phf_generator", + "phf_shared", + "proc-macro2", + "quote", + "syn", +] + +[[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.13" @@ -2961,6 +4238,18 @@ version = "0.2.17" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "a89322df9ebe1c1578d689c92318e070967d1042b512afbe49518723f4e6d5cd" +[[package]] +name = "pin-utils" +version = "0.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8b870d8c151b6f2fb93e84a13146138f05d02ed11c7e7c54f8826aaaf7c9f184" + +[[package]] +name = "pin-weak" +version = "1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b330c9d1b92dfe68442ca20b009c717d5f0b1e3cf4965e62f704c3c6e95a1305" + [[package]] name = "piper" version = "0.2.5" @@ -3015,18 +4304,12 @@ checksum = "5d0e4f59085d47d8241c88ead0f274e8a0cb551f3625263c05eb8dd897c34218" dependencies = [ "cfg-if", "concurrent-queue", - "hermit-abi", + "hermit-abi 0.5.2", "pin-project-lite", "rustix 1.1.4", "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 = "poly1305" version = "0.8.0" @@ -3038,19 +4321,22 @@ dependencies = [ "universal-hash", ] +[[package]] +name = "polycool" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "50596ddc09eb5ad5f75cacd40209568e66df71baf86e1499a0e99c4cff12a5a6" +dependencies = [ + "arrayvec", +] + [[package]] name = "portable-atomic" version = "1.13.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "c33a9471896f1c69cecef8d20cbe2f7accd12527ce60845ff44c153bb2a21b49" - -[[package]] -name = "portable-atomic-util" -version = "0.2.7" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c2a106d1259c23fac8e543272398ae0e3c0b8d33c88ed73d0cc71b0f1d902618" dependencies = [ - "portable-atomic", + "critical-section", ] [[package]] @@ -3059,6 +4345,8 @@ version = "0.1.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "0103b1cef7ec0cf76490e969665504990193874ea05c85ff9bab8b911d0a0564" dependencies = [ + "serde_core", + "writeable", "zerovec", ] @@ -3072,10 +4360,14 @@ dependencies = [ ] [[package]] -name = "presser" -version = "0.3.1" +name = "prettyplease" +version = "0.2.37" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e8cf8e6a8aa66ce33f63993ffc4ea4271eb5b0530a9002db8455ea6050c77bfa" +checksum = "479ca8adacdd7ce8f1fb39ce9ecccbfe93a3f1344b3d0d97f20bc0196208f62b" +dependencies = [ + "proc-macro2", + "syn", +] [[package]] name = "primal-check" @@ -3109,6 +4401,61 @@ name = "profiling" version = "1.0.18" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "3d595e54a326bc53c1c197b32d295e14b169e3cfeaa8dc82b529f947fba6bcf5" +dependencies = [ + "profiling-procmacros", +] + +[[package]] +name = "profiling-procmacros" +version = "1.0.18" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4488a4a36b9a4ba6b9334a32a39971f77c1436ec82c38707bce707699cc3bbcb" +dependencies = [ + "quote", + "syn", +] + +[[package]] +name = "pulldown-cmark" +version = "0.13.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e9f068eba8e7071c5f9511831b44f32c740d5adf574e990f946ddb53db2f314e" +dependencies = [ + "bitflags 2.13.0", + "getopts", + "memchr", + "pulldown-cmark-escape", + "unicase", +] + +[[package]] +name = "pulldown-cmark-escape" +version = "0.11.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "007d8adb5ddab6f8e3f491ac63566a7d5002cc7ed73901f72057943fa71ae1ae" + +[[package]] +name = "pulp" +version = "0.22.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "046aa45b989642ec2e4717c8e72d677b13edd831a4d3b6cf37d9a3e54912496a" +dependencies = [ + "bytemuck", + "cfg-if", + "libm", + "num-complex", + "paste", + "pulp-wasm-simd-flag", + "raw-cpuid", + "reborrow", + "version_check", +] + +[[package]] +name = "pulp-wasm-simd-flag" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1d8f70e07b9c3962945a74e59ca1c511bba65b6419468acc217c457d93f3c740" [[package]] name = "pxfm" @@ -3116,6 +4463,15 @@ version = "0.1.30" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d55d956fa96f5ec02be2e13af0e20391a5aa83d6a074e3ad368959d0fab299ea" +[[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" @@ -3159,10 +4515,20 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "5ca0ecfa931c29007047d1bc58e623ab12e5590e8c7cc53200d5202b69266d8a" dependencies = [ "libc", - "rand_chacha", + "rand_chacha 0.3.1", "rand_core 0.6.4", ] +[[package]] +name = "rand" +version = "0.9.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b9ef1d0d795eb7d84685bca4f72f3649f064e6641543d3a8c415898726a57b41" +dependencies = [ + "rand_chacha 0.9.0", + "rand_core 0.9.5", +] + [[package]] name = "rand" version = "0.10.2" @@ -3184,6 +4550,16 @@ dependencies = [ "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.5", +] + [[package]] name = "rand_core" version = "0.6.4" @@ -3193,6 +4569,15 @@ dependencies = [ "getrandom 0.2.17", ] +[[package]] +name = "rand_core" +version = "0.9.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "76afc826de14238e6e8c374ddcc1fa19e374fd8dd986b0d2af0d02377261d83c" +dependencies = [ + "getrandom 0.3.4", +] + [[package]] name = "rand_core" version = "0.10.1" @@ -3210,10 +4595,63 @@ dependencies = [ ] [[package]] -name = "range-alloc" -version = "0.1.5" +name = "rav1e" +version = "0.8.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ca45419789ae5a7899559e9512e58ca889e41f04f1f2445e9f4b290ceccd1d08" +checksum = "43b6dd56e85d9483277cde964fd1bdb0428de4fec5ebba7540995639a21cb32b" +dependencies = [ + "aligned-vec", + "arbitrary", + "arg_enum_proc_macro", + "arrayvec", + "av-scenechange", + "av1-grain", + "bitstream-io", + "built", + "cfg-if", + "interpolate_name", + "itertools 0.14.0", + "libc", + "libfuzzer-sys", + "log", + "maybe-rayon", + "new_debug_unreachable", + "noop_proc_macro", + "num-derive", + "num-traits", + "paste", + "profiling", + "rand 0.9.5", + "rand_chacha 0.9.0", + "simd_helpers", + "thiserror 2.0.18", + "v_frame", + "wasm-bindgen", +] + +[[package]] +name = "ravif" +version = "0.13.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e52310197d971b0f5be7fe6b57530dcd27beb35c1b013f29d66c1ad73fbbcc45" +dependencies = [ + "avif-serialize", + "imgref", + "loop9", + "quick-error", + "rav1e", + "rayon", + "rgb", +] + +[[package]] +name = "raw-cpuid" +version = "11.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "498cd0dc59d73224351ee52a95fee0f1a617a2eae0e7d9d720cc622c73a54186" +dependencies = [ + "bitflags 2.13.0", +] [[package]] name = "raw-window-handle" @@ -3221,6 +4659,48 @@ version = "0.6.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "20675572f6f24e9e76ef639bc5552774ed45f1c30e2951e1e99c59888861c539" +[[package]] +name = "raw-window-metal" +version = "1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "40d213455a5f1dc59214213c7330e074ddf8114c9a42411eb890c767357ce135" +dependencies = [ + "objc2 0.6.4", + "objc2-core-foundation", + "objc2-foundation 0.3.2", + "objc2-quartz-core 0.3.2", +] + +[[package]] +name = "rayon" +version = "1.12.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fb39b166781f92d482534ef4b4b1b2568f42613b53e5b6c160e24cfbfa30926d" +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.39.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c4ed38b89c2c77ff968c524145ad65fb010f38af5c7a224b53b81d47ac2daa81" +dependencies = [ + "bytemuck", + "font-types", +] + [[package]] name = "realfft" version = "3.5.0" @@ -3230,6 +4710,12 @@ dependencies = [ "rustfft", ] +[[package]] +name = "reborrow" +version = "0.5.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "03251193000f4bd3b042892be858ee50e8b3719f2b08e5833ac4353724632430" + [[package]] name = "redox_syscall" version = "0.4.1" @@ -3268,6 +4754,18 @@ dependencies = [ "thiserror 2.0.18", ] +[[package]] +name = "regex" +version = "1.13.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2a0e75113e14dc5acb068cd0786884f214f1312650a3d36d269f5c4f3cdee8a2" +dependencies = [ + "aho-corasick", + "memchr", + "regex-automata", + "regex-syntax", +] + [[package]] name = "regex-automata" version = "0.4.14" @@ -3286,10 +4784,30 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d6f6ff9a378485b298a5286656da665ba74413d36db0979633275d2e708145d4" [[package]] -name = "renderdoc-sys" -version = "1.1.0" +name = "resvg" +version = "0.47.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "19b30a45b0cd0bcca8037f3d0dc3421eaf95327a17cad11964fb8179b4fc4832" +checksum = "9be183ad6a216aa96f33e4c8033b0988b8b3ea6fd2359d19af5bac4643fd8e81" +dependencies = [ + "gif", + "image-webp", + "log", + "pico-args", + "rgb", + "svgtypes", + "tiny-skia 0.12.0", + "usvg", + "zune-jpeg", +] + +[[package]] +name = "rgb" +version = "0.8.53" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "47b34b781b31e5d73e9fbc8689c70551fd1ade9a19e3e28cfec8580a79290cc4" +dependencies = [ + "bytemuck", +] [[package]] name = "rodio" @@ -3307,6 +4825,40 @@ dependencies = [ "thiserror 2.0.18", ] +[[package]] +name = "rowan" +version = "0.16.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "417a3a9f582e349834051b8a10c8d71ca88da4211e4093528e36b9845f6b5f21" +dependencies = [ + "countme", + "hashbrown 0.14.5", + "rustc-hash 1.1.0", + "text-size", +] + +[[package]] +name = "roxmltree" +version = "0.21.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f1964b10c76125c36f8afe190065a4bf9a87bf324842c05701330bba9f1cacbb" +dependencies = [ + "memchr", +] + +[[package]] +name = "rspolib" +version = "0.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4fda9a7796aff63a7b1b39ccc93fffaaf65e20042984b4843041a49ca4677535" +dependencies = [ + "lazy_static", + "natord", + "snafu", + "unicode-linebreak", + "unicode-width", +] + [[package]] name = "rtrb" version = "0.3.4" @@ -3380,6 +4932,24 @@ version = "1.0.23" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "cf54715a573b99ac80df0bc206da022bcd442c974952c7b9720069370852e21f" +[[package]] +name = "rustybuzz" +version = "0.20.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fd3c7c96f8a08ee34eff8857b11b49b07d71d1c3f4e88f8a88d4c9e9f90b1702" +dependencies = [ + "bitflags 2.13.0", + "bytemuck", + "core_maths", + "log", + "smallvec", + "ttf-parser", + "unicode-bidi-mirroring", + "unicode-ccc", + "unicode-properties", + "unicode-script", +] + [[package]] name = "same-file" version = "1.0.6" @@ -3395,6 +4965,12 @@ version = "1.0.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e1cf6437eb19a8f4a6cc0f7dca544973b0b78843adbfeb3683d1a94a0024a294" +[[package]] +name = "scoped-tls-hkt" +version = "0.1.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e9603871ffe5df3ac39cb624790c296dbd47a400d202f56bf3e414045099524d" + [[package]] name = "scopeguard" version = "1.2.0" @@ -3411,7 +4987,7 @@ dependencies = [ "log", "memmap2", "smithay-client-toolkit 0.19.2", - "tiny-skia", + "tiny-skia 0.11.4", ] [[package]] @@ -3430,6 +5006,16 @@ dependencies = [ "serde_derive", ] +[[package]] +name = "serde_bytes" +version = "0.11.19" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a5d440709e79d88e51ac01c4b72fc6cb7314017bb7da9eeff678aa94c10e3ea8" +dependencies = [ + "serde", + "serde_core", +] + [[package]] name = "serde_core" version = "1.0.228" @@ -3474,6 +5060,15 @@ dependencies = [ "syn", ] +[[package]] +name = "serde_spanned" +version = "1.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6662b5879511e06e8999a8a235d848113e942c9124f211511b16466ee2995f26" +dependencies = [ + "serde_core", +] + [[package]] name = "sha2" version = "0.10.9" @@ -3505,6 +5100,12 @@ dependencies = [ "lazy_static", ] +[[package]] +name = "shlex" +version = "1.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0fda2ff0d084019ba4d7c6f371c95d8fd75ce3524c3cb8fb653a3023f6323e64" + [[package]] name = "shlex" version = "2.0.1" @@ -3546,18 +5147,126 @@ dependencies = [ "simdutf8", ] +[[package]] +name = "simd_helpers" +version = "0.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "95890f873bec569a0362c235787f3aca6e1e887302ba4840839bcc6459c42da6" +dependencies = [ + "quote", +] + [[package]] name = "simdutf8" version = "0.1.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e3a9fe34e3e7a50316060351f37187a3f546bce95496156754b601a5fa71b76e" +[[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.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8ee5873ec9cce0195efcb7a4e9507a04cd49aec9c83d0389df45b1ef7ba2e649" + +[[package]] +name = "skia-bindings" +version = "0.99.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3e2d1c3ebd697c0cbded0145e9204a38fa6b268446051b7196d0a096414ea7f3" +dependencies = [ + "bindgen", + "cc", + "flate2", + "heck", + "pkg-config", + "regex", + "serde_json", + "tar", + "toml", +] + +[[package]] +name = "skia-safe" +version = "0.99.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9f512ac418a64194842dd05566320805dad1c957c521039db2486fd6368865bc" +dependencies = [ + "bitflags 2.13.0", + "skia-bindings", + "windows", +] + +[[package]] +name = "skrifa" +version = "0.42.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0c34617370ae968efb7161bb2beb517d9084659aae19e24b89e3db25b46e4564" +dependencies = [ + "bytemuck", + "read-fonts", +] + [[package]] name = "slab" version = "0.4.12" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "0c790de23124f9ab44544d7ac05d60440adc586479ce501c1d6d7da3cd8c9cf5" +[[package]] +name = "slint" +version = "1.17.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ce12038f57f8f3a423564b0c9301a2ab3fda4246e4cbadd6b97acf9124bdc315" +dependencies = [ + "const-field-offset", + "i-slint-backend-selector", + "i-slint-common", + "i-slint-core", + "i-slint-core-macros", + "i-slint-renderer-femtovg", + "i-slint-renderer-software", + "num-traits", + "once_cell", + "pin-weak", + "slint-macros", + "unicode-segmentation", + "vtable", +] + +[[package]] +name = "slint-build" +version = "1.17.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "76684c11441f775b10bbc7b0e6b3016a39f17efdd659ffca112f17cfd6bdaed1" +dependencies = [ + "derive_more", + "fontique", + "i-slint-compiler", + "spin_on", + "toml_edit", +] + +[[package]] +name = "slint-macros" +version = "1.17.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9f2a763e72e1e53c496f571d36327e777da1eb0d9e58fceb83890c2a83ee7223" +dependencies = [ + "i-slint-compiler", + "proc-macro2", + "quote", + "spin_on", +] + [[package]] name = "slotmap" version = "1.1.1" @@ -3645,6 +5354,37 @@ dependencies = [ "serde", ] +[[package]] +name = "smol_str" +version = "0.3.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4aaa7368fcf4852a4c2dd92df0cace6a71f2091ca0a23391ce7f3a31833f1523" +dependencies = [ + "borsh", + "serde_core", +] + +[[package]] +name = "snafu" +version = "0.8.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6e84b3f4eacbf3a1ce05eac6763b4d629d60cbc94d632e4092c54ade71f1e1a2" +dependencies = [ + "snafu-derive", +] + +[[package]] +name = "snafu-derive" +version = "0.8.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c1c97747dbf44bb1ca44a561ece23508e99cb592e862f22222dcf42f51d1e451" +dependencies = [ + "heck", + "proc-macro2", + "quote", + "syn", +] + [[package]] name = "socket2" version = "0.6.4" @@ -3656,12 +5396,43 @@ dependencies = [ ] [[package]] -name = "spirv" -version = "0.3.0+sdk-1.3.268.0" +name = "softbuffer" +version = "0.4.8" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "eda41003dc44290527a59b13432d4a0379379fa074b70174882adfbdfd917844" +checksum = "aac18da81ebbf05109ab275b157c22a653bb3c12cf884450179942f81bcbf6c3" dependencies = [ - "bitflags 2.13.0", + "as-raw-xcb-connection", + "bytemuck", + "fastrand", + "js-sys", + "memmap2", + "ndk", + "objc2 0.6.4", + "objc2-core-foundation", + "objc2-core-graphics", + "objc2-foundation 0.3.2", + "objc2-quartz-core 0.3.2", + "raw-window-handle", + "redox_syscall 0.5.18", + "rustix 1.1.4", + "tiny-xlib", + "tracing", + "wasm-bindgen", + "wayland-backend", + "wayland-client", + "wayland-sys", + "web-sys", + "windows-sys 0.61.2", + "x11rb", +] + +[[package]] +name = "spin_on" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "076e103ed41b9864aa838287efe5f4e3a7a0362dd00671ae62a212e5e4612da2" +dependencies = [ + "pin-utils", ] [[package]] @@ -3697,6 +5468,30 @@ name = "strict-num" version = "0.1.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "6637bab7722d379c8b41ba849228d680cc12d0a45ba1fa2b48f2a30577a06731" +dependencies = [ + "float-cmp", +] + +[[package]] +name = "strum" +version = "0.28.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9628de9b8791db39ceda2b119bbe13134770b56c138ec1d3af810d045c04f9bd" +dependencies = [ + "strum_macros", +] + +[[package]] +name = "strum_macros" +version = "0.28.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ab85eea0270ee17587ed4156089e10b9e6880ee688791d45a905f5b1ca36f664" +dependencies = [ + "heck", + "proc-macro2", + "quote", + "syn", +] [[package]] name = "subtle" @@ -3704,6 +5499,27 @@ version = "2.6.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "13c2bddecc57b384dee18652358fb23172facb8a2c51ccc10d74c157bdea3292" +[[package]] +name = "svgtypes" +version = "0.16.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "695b5790b3131dafa99b3bbfd25a216edb3d216dad9ca208d4657bfb8f2abc3d" +dependencies = [ + "kurbo", + "siphasher", +] + +[[package]] +name = "swash" +version = "0.2.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0811b01ca2c4e8718760713911feaf4675c24f94e50530a015ec646cfb622f7c" +dependencies = [ + "skrifa", + "yazi", + "zeno", +] + [[package]] name = "symphonia" version = "0.5.5" @@ -3873,6 +5689,41 @@ dependencies = [ "syn", ] +[[package]] +name = "sys-locale" +version = "0.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8eab9a99a024a169fe8a903cf9d4a3b3601109bcc13bd9e3c6fff259138626c4" +dependencies = [ + "js-sys", + "libc", + "wasm-bindgen", + "web-sys", +] + +[[package]] +name = "taffy" +version = "0.10.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "aea22054047c16c3f34d3ac473a2170be1424b1115b2a3adcf28cfb067c88859" +dependencies = [ + "arrayvec", + "grid", + "serde", + "slotmap", +] + +[[package]] +name = "tar" +version = "0.4.46" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3f6221d9a6003c78398e3b239969f352578258df48c8eb051caadae0015bc840" +dependencies = [ + "filetime", + "libc", + "xattr", +] + [[package]] name = "tempfile" version = "3.27.0" @@ -3887,13 +5738,10 @@ dependencies = [ ] [[package]] -name = "termcolor" -version = "1.4.1" +name = "text-size" +version = "1.1.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "06794f8f6c5c898b3275aebefa6b8a1cb24cd2c6c79397ab15774837a0bc5755" -dependencies = [ - "winapi-util", -] +checksum = "f18aa187839b2bdb1ad2fa35ead8c4c2976b64e4363c386d45ac0f7ee85c9233" [[package]] name = "thiserror" @@ -3969,7 +5817,22 @@ dependencies = [ "bytemuck", "cfg-if", "log", - "tiny-skia-path", + "tiny-skia-path 0.11.4", +] + +[[package]] +name = "tiny-skia" +version = "0.12.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "47ffee5eaaf5527f630fb0e356b90ebdec84d5d18d937c5e440350f88c5a91ea" +dependencies = [ + "arrayref", + "arrayvec", + "bytemuck", + "cfg-if", + "log", + "png", + "tiny-skia-path 0.12.0", ] [[package]] @@ -3983,6 +5846,30 @@ dependencies = [ "strict-num", ] +[[package]] +name = "tiny-skia-path" +version = "0.12.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "edca365c3faccca67d06593c5980fa6c57687de727a03131735bb85f01fdeeb9" +dependencies = [ + "arrayref", + "bytemuck", + "strict-num", +] + +[[package]] +name = "tiny-xlib" +version = "0.2.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a90a0ca3ee6a69f2ad28fd11621a4c3f03b371f366be500b64df260c4ffbafb4" +dependencies = [ + "as-raw-xcb-connection", + "ctor", + "libloading", + "pkg-config", + "tracing", +] + [[package]] name = "tinystr" version = "0.8.3" @@ -3990,9 +5877,25 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "c8323304221c2a851516f22236c5722a72eaa19749016521d6dff0824447d96d" dependencies = [ "displaydoc", + "serde_core", "zerovec", ] +[[package]] +name = "tinyvec" +version = "1.12.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bb4ebadaa0af04fab11ae01eb5f9fdb5f9c5b875506e210e71c07873528baa7f" +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.52.3" @@ -4021,6 +5924,21 @@ dependencies = [ "syn", ] +[[package]] +name = "toml" +version = "1.1.2+spec-1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "81f3d15e84cbcd896376e6730314d59fb5a87f31e4b038454184435cd57defee" +dependencies = [ + "indexmap", + "serde_core", + "serde_spanned", + "toml_datetime", + "toml_parser", + "toml_writer", + "winnow", +] + [[package]] name = "toml_datetime" version = "1.1.1+spec-1.1.0" @@ -4039,6 +5957,7 @@ dependencies = [ "indexmap", "toml_datetime", "toml_parser", + "toml_writer", "winnow", ] @@ -4051,6 +5970,12 @@ dependencies = [ "winnow", ] +[[package]] +name = "toml_writer" +version = "1.1.1+spec-1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "756daf9b1013ebe47a8776667b466417e2d4c5679d441c26230efd9ef78692db" + [[package]] name = "tracing" version = "0.1.44" @@ -4128,14 +6053,18 @@ 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" +name = "typed-index-collections" +version = "3.5.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "cb30dbbd9036155e74adad6812e9898d03ec374946234fbcebd5dfc7b9187b90" +checksum = "898160f1dfd383b4e92e17f0512a7d62f3c51c44937b23b6ffc3a1614a8eaccd" dependencies = [ - "rustc-hash 2.1.3", + "bincode", + "serde", ] [[package]] @@ -4144,6 +6073,18 @@ version = "1.20.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b6f5e870be6c3b371b77fe0ee0bafb859fa4964b4404c27de1d380043c4dda20" +[[package]] +name = "udev" +version = "0.9.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "af4e37e9ea4401fc841ff54b9ddfc9be1079b1e89434c1a6a865dd68980f7e9f" +dependencies = [ + "io-lifetimes", + "libc", + "libudev-sys", + "pkg-config", +] + [[package]] name = "uds_windows" version = "1.2.1" @@ -4155,24 +6096,78 @@ dependencies = [ "windows-sys 0.61.2", ] +[[package]] +name = "unicase" +version = "2.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dbc4bc3a9f746d862c45cb89d705aa10f187bb96c76001afab07a0d35ce60142" + +[[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.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5dfa6e8c60bb66d49db113e0125ee8711b7647b5579dc7f5f19c42357ed039fe" + +[[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.24" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e6e4313cd5fcd3dad5cafa179702e2b244f760991f45397d14d4ebf38247da75" +[[package]] +name = "unicode-linebreak" +version = "0.1.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3b09c83c3c29d37506a3e260c08c03743a6bb66a9cd432c6934ab501a190571f" + +[[package]] +name = "unicode-properties" +version = "0.1.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7df058c713841ad818f1dc5d3fd88063241cc61f49f5fbea4b951e8cf5a8d71d" + +[[package]] +name = "unicode-script" +version = "0.5.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "383ad40bb927465ec0ce7720e033cb4ca06912855fc35db31b5755d0de75b1ee" + [[package]] name = "unicode-segmentation" version = "1.13.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "c6f5d3c3b1bf09027a88a6bc961fc00497d651009560b5463668dc81b0fa87a8" +[[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.2.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b4ac048d71ede7ee76d585517add45da530660ef4390e49b098733c6e897f254" +[[package]] +name = "unicode-xid" +version = "0.2.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ebc1c04c71510c7f702b52b7c350734c9ff1295c464a03335b00bb84fc54f853" + [[package]] name = "universal-hash" version = "0.5.1" @@ -4183,6 +6178,12 @@ dependencies = [ "subtle", ] +[[package]] +name = "unty" +version = "0.0.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6d49784317cd0d1ee7ec5c716dd598ec5b4483ea832a2dced265471cc0f690ae" + [[package]] name = "url" version = "2.5.8" @@ -4195,6 +6196,34 @@ dependencies = [ "serde", ] +[[package]] +name = "usvg" +version = "0.47.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d46cf96c5f498d36b7a9693bc6a7075c0bb9303189d61b2249b0dc3d309c07de" +dependencies = [ + "base64", + "data-url", + "flate2", + "fontdb", + "imagesize", + "kurbo", + "log", + "pico-args", + "roxmltree", + "rustybuzz", + "simplecss", + "siphasher", + "strict-num", + "svgtypes", + "tiny-skia-path 0.12.0", + "ttf-parser", + "unicode-bidi", + "unicode-script", + "unicode-vo", + "xmlwriter", +] + [[package]] name = "utf8_iter" version = "1.0.4" @@ -4213,6 +6242,17 @@ dependencies = [ "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 = "valuable" version = "0.1.1" @@ -4235,7 +6275,6 @@ dependencies = [ "cpal 0.18.1", "dirs", "ed25519-dalek", - "eframe", "hex", "hkdf", "nnnoiseless", @@ -4243,8 +6282,11 @@ dependencies = [ "rand 0.8.6", "rodio", "serde", + "serde_bytes", "serde_json", "sha2 0.11.0", + "slint", + "slint-build", "tokio", "tracing", "tracing-subscriber", @@ -4252,6 +6294,29 @@ dependencies = [ "x25519-dalek", ] +[[package]] +name = "vtable" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5ea953f65593feb5287b022b83c35046d1ff49c7417b6f5851a079d8a231fe31" +dependencies = [ + "const-field-offset", + "portable-atomic", + "stable_deref_trait", + "vtable-macro", +] + +[[package]] +name = "vtable-macro" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "482a37f9777480673d3a99c80ac2edc69f92382230440946855fa27ebc47cba6" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + [[package]] name = "walkdir" version = "2.5.0" @@ -4509,157 +6574,6 @@ version = "0.1.12" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "a28ac98ddc8b9274cb41bb4d9d4d5c425b6020c50c46f25559911905610b4a88" -[[package]] -name = "wgpu" -version = "27.0.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "bfe68bac7cde125de7a731c3400723cadaaf1703795ad3f4805f187459cd7a77" -dependencies = [ - "arrayvec", - "bitflags 2.13.0", - "cfg-if", - "cfg_aliases", - "document-features", - "hashbrown 0.16.1", - "js-sys", - "log", - "naga", - "parking_lot", - "portable-atomic", - "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 = "27.0.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "27a75de515543b1897b26119f93731b385a19aea165a1ec5f0e3acecc229cae7" -dependencies = [ - "arrayvec", - "bit-set", - "bit-vec", - "bitflags 2.13.0", - "bytemuck", - "cfg_aliases", - "document-features", - "hashbrown 0.16.1", - "indexmap", - "log", - "naga", - "once_cell", - "parking_lot", - "portable-atomic", - "profiling", - "raw-window-handle", - "rustc-hash 1.1.0", - "smallvec", - "thiserror 2.0.18", - "wgpu-core-deps-apple", - "wgpu-core-deps-emscripten", - "wgpu-core-deps-windows-linux-android", - "wgpu-hal", - "wgpu-types", -] - -[[package]] -name = "wgpu-core-deps-apple" -version = "27.0.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0772ae958e9be0c729561d5e3fd9a19679bcdfb945b8b1a1969d9bfe8056d233" -dependencies = [ - "wgpu-hal", -] - -[[package]] -name = "wgpu-core-deps-emscripten" -version = "27.0.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b06ac3444a95b0813ecfd81ddb2774b66220b264b3e2031152a4a29fda4da6b5" -dependencies = [ - "wgpu-hal", -] - -[[package]] -name = "wgpu-core-deps-windows-linux-android" -version = "27.0.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "71197027d61a71748e4120f05a9242b2ad142e3c01f8c1b47707945a879a03c3" -dependencies = [ - "wgpu-hal", -] - -[[package]] -name = "wgpu-hal" -version = "27.0.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5b21cb61c57ee198bc4aff71aeadff4cbb80b927beb912506af9c780d64313ce" -dependencies = [ - "android_system_properties", - "arrayvec", - "ash", - "bit-set", - "bitflags 2.13.0", - "block", - "bytemuck", - "cfg-if", - "cfg_aliases", - "core-graphics-types 0.2.0", - "glow", - "glutin_wgl_sys", - "gpu-alloc", - "gpu-allocator", - "gpu-descriptor", - "hashbrown 0.16.1", - "js-sys", - "khronos-egl", - "libc", - "libloading", - "log", - "metal", - "naga", - "ndk-sys", - "objc", - "once_cell", - "ordered-float", - "parking_lot", - "portable-atomic", - "portable-atomic-util", - "profiling", - "range-alloc", - "raw-window-handle", - "renderdoc-sys", - "smallvec", - "thiserror 2.0.18", - "wasm-bindgen", - "web-sys", - "wgpu-types", - "windows 0.58.0", - "windows-core 0.58.0", -] - -[[package]] -name = "wgpu-types" -version = "27.0.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "afdcf84c395990db737f2dd91628706cb31e86d72e53482320d368e52b5da5eb" -dependencies = [ - "bitflags 2.13.0", - "bytemuck", - "js-sys", - "log", - "thiserror 2.0.18", - "web-sys", -] - [[package]] name = "winapi-util" version = "0.1.11" @@ -4669,48 +6583,16 @@ dependencies = [ "windows-sys 0.61.2", ] -[[package]] -name = "windows" -version = "0.58.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "dd04d41d93c4992d421894c18c8b43496aa748dd4c081bac0dc93eb0489272b6" -dependencies = [ - "windows-core 0.58.0", - "windows-targets 0.52.6", -] - -[[package]] -name = "windows" -version = "0.61.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9babd3a767a4c1aef6900409f85f5d53ce2544ccdfaa86dad48c91782c6d6893" -dependencies = [ - "windows-collections 0.2.0", - "windows-core 0.61.2", - "windows-future 0.2.1", - "windows-link 0.1.3", - "windows-numerics 0.2.0", -] - [[package]] name = "windows" version = "0.62.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "527fadee13e0c05939a6a05d5bd6eec6cd2e3dbd648b9f8e447c6518133d8580" dependencies = [ - "windows-collections 0.3.2", - "windows-core 0.62.2", - "windows-future 0.3.2", - "windows-numerics 0.3.1", -] - -[[package]] -name = "windows-collections" -version = "0.2.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3beeceb5e5cfd9eb1d76b381630e82c4241ccd0d27f1a39ed41b2760b255c5e8" -dependencies = [ - "windows-core 0.61.2", + "windows-collections", + "windows-core", + "windows-future", + "windows-numerics", ] [[package]] @@ -4719,33 +6601,7 @@ version = "0.3.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "23b2d95af1a8a14a3c7367e1ed4fc9c20e0a26e79551b1454d72583c97cc6610" dependencies = [ - "windows-core 0.62.2", -] - -[[package]] -name = "windows-core" -version = "0.58.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6ba6d44ec8c2591c134257ce647b7ea6b20335bf6379a27dac5f1641fcf59f99" -dependencies = [ - "windows-implement 0.58.0", - "windows-interface 0.58.0", - "windows-result 0.2.0", - "windows-strings 0.1.0", - "windows-targets 0.52.6", -] - -[[package]] -name = "windows-core" -version = "0.61.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c0fdd3ddb90610c7638aa2b3a3ab2904fb9e5cdbecc643ddb3647212781c4ae3" -dependencies = [ - "windows-implement 0.60.2", - "windows-interface 0.59.3", - "windows-link 0.1.3", - "windows-result 0.3.4", - "windows-strings 0.4.2", + "windows-core", ] [[package]] @@ -4754,22 +6610,11 @@ version = "0.62.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b8e83a14d34d0623b51dce9581199302a221863196a1dde71a7663a4c2be9deb" dependencies = [ - "windows-implement 0.60.2", - "windows-interface 0.59.3", - "windows-link 0.2.1", - "windows-result 0.4.1", - "windows-strings 0.5.1", -] - -[[package]] -name = "windows-future" -version = "0.2.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "fc6a41e98427b19fe4b73c550f060b59fa592d7d686537eebf9385621bfbad8e" -dependencies = [ - "windows-core 0.61.2", - "windows-link 0.1.3", - "windows-threading 0.1.0", + "windows-implement", + "windows-interface", + "windows-link", + "windows-result", + "windows-strings", ] [[package]] @@ -4778,20 +6623,9 @@ version = "0.3.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e1d6f90251fe18a279739e78025bd6ddc52a7e22f921070ccdc67dde84c605cb" dependencies = [ - "windows-core 0.62.2", - "windows-link 0.2.1", - "windows-threading 0.2.1", -] - -[[package]] -name = "windows-implement" -version = "0.58.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2bbd5b46c938e506ecbce286b6628a02171d56153ba733b6c741fc627ec9579b" -dependencies = [ - "proc-macro2", - "quote", - "syn", + "windows-core", + "windows-link", + "windows-threading", ] [[package]] @@ -4805,17 +6639,6 @@ dependencies = [ "syn", ] -[[package]] -name = "windows-interface" -version = "0.58.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "053c4c462dc91d3b1504c6fe5a726dd15e216ba718e84a0e46a88fbe5ded3515" -dependencies = [ - "proc-macro2", - "quote", - "syn", -] - [[package]] name = "windows-interface" version = "0.59.3" @@ -4827,54 +6650,20 @@ dependencies = [ "syn", ] -[[package]] -name = "windows-link" -version = "0.1.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5e6ad25900d524eaabdbbb96d20b4311e1e7ae1699af4fb28c17ae66c80d798a" - [[package]] name = "windows-link" version = "0.2.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "f0805222e57f7521d6a62e36fa9163bc891acd422f971defe97d64e70d0a4fe5" -[[package]] -name = "windows-numerics" -version = "0.2.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9150af68066c4c5c07ddc0ce30421554771e528bde427614c61038bc2c92c2b1" -dependencies = [ - "windows-core 0.61.2", - "windows-link 0.1.3", -] - [[package]] name = "windows-numerics" version = "0.3.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "6e2e40844ac143cdb44aead537bbf727de9b044e107a0f1220392177d15b0f26" dependencies = [ - "windows-core 0.62.2", - "windows-link 0.2.1", -] - -[[package]] -name = "windows-result" -version = "0.2.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1d1043d8214f791817bab27572aaa8af63732e11bf84aa21a45a78d6c317ae0e" -dependencies = [ - "windows-targets 0.52.6", -] - -[[package]] -name = "windows-result" -version = "0.3.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "56f42bd332cc6c8eac5af113fc0c1fd6a8fd2aa08a0119358686e5160d0586c6" -dependencies = [ - "windows-link 0.1.3", + "windows-core", + "windows-link", ] [[package]] @@ -4883,26 +6672,7 @@ version = "0.4.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "7781fa89eaf60850ac3d2da7af8e5242a5ea78d1a11c49bf2910bb5a73853eb5" dependencies = [ - "windows-link 0.2.1", -] - -[[package]] -name = "windows-strings" -version = "0.1.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4cd9b125c486025df0eabcb585e62173c6c9eddcec5d117d3b6e8c30e2ee4d10" -dependencies = [ - "windows-result 0.2.0", - "windows-targets 0.52.6", -] - -[[package]] -name = "windows-strings" -version = "0.4.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "56e6c93f3a0c3b36176cb1327a4958a0353d5d166c2a35cb268ace15e91d3b57" -dependencies = [ - "windows-link 0.1.3", + "windows-link", ] [[package]] @@ -4911,7 +6681,7 @@ version = "0.5.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "7837d08f69c77cf6b07689544538e017c1bfcf57e34b4c0ff58e6c2cd3b37091" dependencies = [ - "windows-link 0.2.1", + "windows-link", ] [[package]] @@ -4923,6 +6693,15 @@ 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" @@ -4941,22 +6720,13 @@ 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 0.2.1", + "windows-link", ] [[package]] @@ -4974,6 +6744,21 @@ dependencies = [ "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" @@ -4983,46 +6768,20 @@ 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_gnullvm", "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 0.2.1", - "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-threading" -version = "0.1.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b66463ad2e0ea3bbf808b7f1d371311c80e115c0b71d60efc142cafbcfb057a6" -dependencies = [ - "windows-link 0.1.3", -] - [[package]] name = "windows-threading" version = "0.2.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "3949bd5b99cafdf1c7ca86b43ca564028dfe27d66958f2470940f73d86d75b37" dependencies = [ - "windows-link 0.2.1", + "windows-link", ] [[package]] @@ -5033,15 +6792,15 @@ checksum = "597a5118570b68bc08d8d59125332c54f1ba9d9adeedeef5b99b02ba2b0698f8" [[package]] name = "windows_aarch64_gnullvm" -version = "0.52.6" +version = "0.48.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "32a4622180e7a0ec044bb555404c800bc9fd9ec262ec147edd5989ccd0c02cd3" +checksum = "2b38e32f0abccf9987a4e3079dfb67dcd799fb61361e53e2882c3cbaf0d905d8" [[package]] name = "windows_aarch64_gnullvm" -version = "0.53.1" +version = "0.52.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a9d8416fa8b42f5c947f8482c43e7d89e73a173cead56d044f6a56104a6d1b53" +checksum = "32a4622180e7a0ec044bb555404c800bc9fd9ec262ec147edd5989ccd0c02cd3" [[package]] name = "windows_aarch64_msvc" @@ -5051,15 +6810,15 @@ checksum = "e08e8864a60f06ef0d0ff4ba04124db8b0fb3be5776a5cd47641e942e58c4d43" [[package]] name = "windows_aarch64_msvc" -version = "0.52.6" +version = "0.48.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "09ec2a7bb152e2252b53fa7803150007879548bc709c039df7627cabbd05d469" +checksum = "dc35310971f3b2dbbf3f0690a219f40e2d9afcf64f9ab7cc1be722937c26b4bc" [[package]] name = "windows_aarch64_msvc" -version = "0.53.1" +version = "0.52.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b9d782e804c2f632e395708e99a94275910eb9100b2114651e04744e9b125006" +checksum = "09ec2a7bb152e2252b53fa7803150007879548bc709c039df7627cabbd05d469" [[package]] name = "windows_i686_gnu" @@ -5069,15 +6828,15 @@ checksum = "c61d927d8da41da96a81f029489353e68739737d3beca43145c8afec9a31a84f" [[package]] name = "windows_i686_gnu" -version = "0.52.6" +version = "0.48.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8e9b5ad5ab802e97eb8e295ac6720e509ee4c243f69d781394014ebfe8bbfa0b" +checksum = "a75915e7def60c94dcef72200b9a8e58e5091744960da64ec734a6c6e9b3743e" [[package]] name = "windows_i686_gnu" -version = "0.53.1" +version = "0.52.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "960e6da069d81e09becb0ca57a65220ddff016ff2d6af6a223cf372a506593a3" +checksum = "8e9b5ad5ab802e97eb8e295ac6720e509ee4c243f69d781394014ebfe8bbfa0b" [[package]] name = "windows_i686_gnullvm" @@ -5085,12 +6844,6 @@ 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" @@ -5099,15 +6852,15 @@ checksum = "44d840b6ec649f480a41c8d80f9c65108b92d89345dd94027bfe06ac444d1060" [[package]] name = "windows_i686_msvc" -version = "0.52.6" +version = "0.48.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "240948bc05c5e7c6dabba28bf89d89ffce3e303022809e73deaefe4f6ec56c66" +checksum = "8f55c233f70c4b27f66c523580f78f1004e8b5a8b659e05a4eb49d4166cca406" [[package]] name = "windows_i686_msvc" -version = "0.53.1" +version = "0.52.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1e7ac75179f18232fe9c285163565a57ef8d3c89254a30685b57d83a38d326c2" +checksum = "240948bc05c5e7c6dabba28bf89d89ffce3e303022809e73deaefe4f6ec56c66" [[package]] name = "windows_x86_64_gnu" @@ -5117,15 +6870,15 @@ checksum = "8de912b8b8feb55c064867cf047dda097f92d51efad5b491dfb98f6bbb70cb36" [[package]] name = "windows_x86_64_gnu" -version = "0.52.6" +version = "0.48.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "147a5c80aabfbf0c7d901cb5895d1de30ef2907eb21fbbab29ca94c5b08b1a78" +checksum = "53d40abd2583d23e4718fddf1ebec84dbff8381c07cae67ff7768bbf19c6718e" [[package]] name = "windows_x86_64_gnu" -version = "0.53.1" +version = "0.52.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9c3842cdd74a865a8066ab39c8a7a473c0778a3f29370b5fd6b4b9aa7df4a499" +checksum = "147a5c80aabfbf0c7d901cb5895d1de30ef2907eb21fbbab29ca94c5b08b1a78" [[package]] name = "windows_x86_64_gnullvm" @@ -5135,15 +6888,15 @@ checksum = "26d41b46a36d453748aedef1486d5c7a85db22e56aff34643984ea85514e94a3" [[package]] name = "windows_x86_64_gnullvm" -version = "0.52.6" +version = "0.48.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "24d5b23dc417412679681396f2b49f3de8c1473deb516bd34410872eff51ed0d" +checksum = "0b7b52767868a23d5bab768e390dc5f5c55825b6d30b86c844ff2dc7414044cc" [[package]] name = "windows_x86_64_gnullvm" -version = "0.53.1" +version = "0.52.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0ffa179e2d07eee8ad8f57493436566c7cc30ac536a3379fdf008f47f6bb7ae1" +checksum = "24d5b23dc417412679681396f2b49f3de8c1473deb516bd34410872eff51ed0d" [[package]] name = "windows_x86_64_msvc" @@ -5153,15 +6906,15 @@ checksum = "9aec5da331524158c6d1a4ac0ab1541149c0b9505fde06423b02f5ef0106b9f0" [[package]] name = "windows_x86_64_msvc" -version = "0.52.6" +version = "0.48.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "589f6da84c646204747d1270a2a5661ea66ed1cced2631d546fdfb155959f9ec" +checksum = "ed94fce61571a4006852b7389a063ab983c02eb1bb37b47f8272ce92d06d9538" [[package]] name = "windows_x86_64_msvc" -version = "0.53.1" +version = "0.52.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d6bbff5f0aada427a1e5a6da5f1f98158182f26556f345ac9e04d36d0ebed650" +checksum = "589f6da84c646204747d1270a2a5661ea66ed1cced2631d546fdfb155959f9ec" [[package]] name = "winit" @@ -5189,7 +6942,7 @@ dependencies = [ "objc2 0.5.2", "objc2-app-kit 0.2.2", "objc2-foundation 0.2.2", - "objc2-ui-kit", + "objc2-ui-kit 0.2.2", "orbclient", "percent-encoding", "pin-project", @@ -5198,7 +6951,7 @@ dependencies = [ "rustix 0.38.44", "sctk-adwaita", "smithay-client-toolkit 0.19.2", - "smol_str", + "smol_str 0.2.2", "tracing", "unicode-segmentation", "wasm-bindgen", @@ -5230,12 +6983,35 @@ version = "0.57.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "1ebf944e87a7c253233ad6766e082e3cd714b5d03812acc24c318f549614536e" +[[package]] +name = "write-fonts" +version = "0.48.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cb731d4c4d93eacc69a1ad2f270f905788a98e4a3438267bcafbe08d3431c8d8" +dependencies = [ + "font-types", + "indexmap", + "kurbo", + "log", + "read-fonts", +] + [[package]] name = "writeable" version = "0.6.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "1ffae5123b2d3fc086436f8834ae3ab053a283cfac8fe0a0b8eaae044768a4c4" +[[package]] +name = "x11-clipboard" +version = "0.9.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "662d74b3d77e396b8e5beb00b9cad6a9eccf40b2ef68cc858784b14c41d535a3" +dependencies = [ + "libc", + "x11rb", +] + [[package]] name = "x11-dl" version = "2.21.0" @@ -5280,12 +7056,33 @@ dependencies = [ "zeroize", ] +[[package]] +name = "xattr" +version = "1.6.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "32e45ad4206f6d2479085147f02bc2ef834ac85886624a23575ae137c8aa8156" +dependencies = [ + "libc", + "rustix 1.1.4", +] + [[package]] name = "xcursor" version = "0.3.10" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "bec9e4a500ca8864c5b47b8b482a73d62e4237670e5b5f1d6b9e3cae50f28f2b" +[[package]] +name = "xkbcommon" +version = "0.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a7a974f48060a14e95705c01f24ad9c3345022f4d97441b8a36beb7ed5c4a02d" +dependencies = [ + "libc", + "memmap2", + "xkeysym", +] + [[package]] name = "xkbcommon-dl" version = "0.4.2" @@ -5311,6 +7108,35 @@ 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 = "yazi" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e01738255b5a16e78bbb83e7fbba0a1e7dd506905cfc53f4622d89015a03fbb5" + +[[package]] +name = "yeslogic-fontconfig-sys" +version = "6.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1d8b8abf912b9a29ff112e1671c97c33636903d13a69712037190e6805af4f76" +dependencies = [ + "dlib", + "once_cell", + "pkg-config", +] + [[package]] name = "yoke" version = "0.8.3" @@ -5431,6 +7257,12 @@ dependencies = [ "zvariant", ] +[[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.54" @@ -5501,6 +7333,7 @@ dependencies = [ "displaydoc", "yoke", "zerofrom", + "zerovec", ] [[package]] @@ -5509,6 +7342,7 @@ version = "0.11.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "90f911cbc359ab6af17377d242225f4d75119aec87ea711a880987b18cd7b239" dependencies = [ + "serde", "yoke", "zerofrom", "zerovec-derive", @@ -5537,6 +7371,15 @@ version = "0.5.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "cb8a0807f7c01457d0379ba880ba6322660448ddebc890ce29bb64da71fb40f9" +[[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.5.15" diff --git a/Cargo.toml b/Cargo.toml index d8fed4d..04b13f9 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -24,10 +24,14 @@ path = "src/main.rs" name = "vnox-e2e-voice" path = "src/bin/e2e_voice.rs" +[build-dependencies] +slint-build = "1" + [dependencies] tokio = { version = "1.52", features = ["full"] } serde = { version = "1", features = ["derive"] } serde_json = "1" +serde_bytes = "0.11" ed25519-dalek = { version = "2", features = ["rand_core"] } x25519-dalek = { version = "2", features = ["static_secrets"] } chacha20poly1305 = "0.10" @@ -44,7 +48,7 @@ opus = "0.3" cpal = "0.18" rodio = "0.22" dirs = "6" -eframe = { version = "0.33", features = ["wgpu"] } +slint = { version = "1", features = ["backend-winit", "compat-1-2"] } nnnoiseless = { version = "0.5.2", default-features = false, optional = true } [features] diff --git a/build.rs b/build.rs new file mode 100644 index 0000000..a3d1e04 --- /dev/null +++ b/build.rs @@ -0,0 +1,3 @@ +fn main() { + slint_build::compile("src/ui/slint/main.slint").unwrap(); +} diff --git a/src/audio/pipeline/playback.rs b/src/audio/pipeline/playback.rs index ee32340..0c25320 100644 --- a/src/audio/pipeline/playback.rs +++ b/src/audio/pipeline/playback.rs @@ -2,15 +2,15 @@ use crate::jitter::{BufferedPacket, JitterBuffer}; use anyhow::Result; use cpal::traits::{DeviceTrait, StreamTrait}; use std::sync::{ + Arc, Mutex, atomic::{AtomicU64, Ordering}, mpsc::TryRecvError, - Arc, Mutex, }; use std::time::{Duration, Instant}; use tracing::{info, warn}; -use crate::audio::config::{self, AudioConfig, OPUS_RATE}; use crate::audio::IncomingVoice; +use crate::audio::config::{self, AudioConfig, OPUS_RATE}; #[derive(Debug, Clone)] pub struct JitterConfig { diff --git a/src/e2ee.rs b/src/e2ee.rs new file mode 100644 index 0000000..7e7a95e --- /dev/null +++ b/src/e2ee.rs @@ -0,0 +1,43 @@ +use chacha20poly1305::aead::Aead; +use chacha20poly1305::{ChaCha20Poly1305, KeyInit, Nonce}; +use hkdf::Hkdf; +use sha2::{Digest, Sha256, Sha512}; +use x25519_dalek::{PublicKey, StaticSecret}; + +use crate::identity::Identity; + +pub fn derive_e2ee_keypair(identity: &Identity) -> (StaticSecret, PublicKey) { + let sk = identity.signing_key().expect("signing key required"); + let hash = Sha512::digest(sk.to_bytes()); + let mut e2ee_seed = [0u8; 32]; + e2ee_seed.copy_from_slice(&hash[..32]); + let static_secret = StaticSecret::from(e2ee_seed); + let public = PublicKey::from(&static_secret); + (static_secret, public) +} + +pub fn compute_shared_secret(my_secret: &StaticSecret, peer_public_bytes: &[u8]) -> [u8; 32] { + let mut peer_pk = [0u8; 32]; + peer_pk.copy_from_slice(peer_public_bytes); + let peer_public = PublicKey::from(peer_pk); + let shared = my_secret.diffie_hellman(&peer_public); + let hk = Hkdf::::new(Some(b"VNOX-E2EE-v1"), shared.as_bytes()); + let mut msg_key = [0u8; 32]; + hk.expand(b"dm-key", &mut msg_key).expect("32 bytes"); + msg_key +} + +pub fn encrypt_message(msg_key: &[u8; 32], message_id: &str, plaintext: &str) -> Vec { + let cipher = ChaCha20Poly1305::new_from_slice(msg_key).unwrap(); + let hash = Sha256::digest(message_id.as_bytes()); + let nonce = Nonce::from_slice(&hash[..12]); + cipher.encrypt(nonce, plaintext.as_bytes()).unwrap() +} + +pub fn decrypt_message(msg_key: &[u8; 32], message_id: &str, ciphertext: &[u8]) -> String { + let cipher = ChaCha20Poly1305::new_from_slice(msg_key).unwrap(); + let hash = Sha256::digest(message_id.as_bytes()); + let nonce = Nonce::from_slice(&hash[..12]); + let plaintext = cipher.decrypt(nonce, ciphertext).unwrap(); + String::from_utf8(plaintext).unwrap() +} diff --git a/src/lib.rs b/src/lib.rs index 16a87a3..d72891e 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -1,4 +1,5 @@ pub mod audio; +pub mod e2ee; pub mod identity; pub mod identity_vault; pub mod jitter; diff --git a/src/main.rs b/src/main.rs index 50223d5..ae07f25 100644 --- a/src/main.rs +++ b/src/main.rs @@ -1,17 +1,36 @@ mod ui; -pub use ui::VnoxApp; -pub use ui::{chat, connect, keybind, members, settings, sidebar, state, theme, titlebar}; +use std::cell::RefCell; +use std::rc::Rc; +use std::time::Duration; + +use crate::ui::slint::MainWindow; use anyhow::Result; +use slint::ComponentHandle; use tracing::info; use tracing_subscriber::EnvFilter; -use vnox_client::{identity, net}; +use vnox_client::audio::AudioPipeline; +use vnox_client::audio::PipelineConfig; +use vnox_client::identity; +use vnox_client::net; +use vnox_client::net::NetCommand; +use vnox_client::net::NetHandle; + +use crate::ui::state::{ConnState, UiState, VoiceUiState}; + +pub struct AppState { + pub(crate) identity: identity::Identity, + pub(crate) net: NetHandle, + pub(crate) ui: UiState, + pub(crate) audio: Option, + pub(crate) voice_channel: Option, + pub(crate) last_pipeline_config: Option, +} fn main() -> Result<()> { - let filter = EnvFilter::try_from_default_env().unwrap_or_else(|_| { - EnvFilter::new("info,wgpu=warn,wgpu_hal=warn,egui_wgpu=warn,naga=warn") - }); + let filter = EnvFilter::try_from_default_env() + .unwrap_or_else(|_| EnvFilter::new("info,wgpu=warn,wgpu_hal=warn,naga=warn")); tracing_subscriber::fmt().with_env_filter(filter).init(); let identity = identity::load_or_generate()?; @@ -24,16 +43,306 @@ fn main() -> Result<()> { let net = net::spawn(identity.clone()); - eframe::run_native( - "Vnox", - eframe::NativeOptions { - viewport: eframe::egui::ViewportBuilder::default() - .with_title("Vnox") - .with_inner_size([900.0, 560.0]) - .with_min_inner_size([600.0, 400.0]), - ..Default::default() - }, - Box::new(|cc| Ok(Box::new(ui::VnoxApp::new(cc, identity, net)))), - ) - .map_err(|e| anyhow::anyhow!("eframe: {e}")) + let state = Rc::new(RefCell::new(AppState { + identity, + net, + ui: UiState::default(), + audio: None, + voice_channel: None, + last_pipeline_config: None, + })); + + let window = MainWindow::new()?; + + wire_signals(&window, &state); + + let timer = slint::Timer::default(); + { + let state = state.clone(); + let weak = window.as_weak(); + timer.start( + slint::TimerMode::Repeated, + Duration::from_millis(16), + move || { + if let Some(win) = weak.upgrade() { + let mut s = state.borrow_mut(); + crate::ui::update::process_net_events(&mut s); + let own_id = s.identity.short_id().to_string(); + crate::ui::bridge::sync_ui(&win, &s.ui, &own_id); + } + }, + ); + } + + window.run()?; + Ok(()) +} + +fn wire_signals(window: &MainWindow, state: &Rc>) { + { + let state = state.clone(); + window.on_channel_selected(move |id| { + let id = id.to_string(); + let mut s = state.borrow_mut(); + s.ui.active_channel = Some(id.clone()); + s.ui.active_dm_id = None; + let net = s.net.clone(); + drop(s); + tokio::spawn(async move { + net.send(NetCommand::JoinChannel { channel_id: id }).await; + }); + }); + } + + { + let state = state.clone(); + window.on_guild_selected(move |idx| { + let mut s = state.borrow_mut(); + s.ui.active_guild_id = if idx >= 0 && (idx as usize) < s.ui.guilds.len() { + Some(s.ui.guilds[idx as usize].guild_id.clone()) + } else { + None + }; + }); + } + + { + let state = state.clone(); + window.on_dm_selected(move |nickname| { + let nick = nickname.to_string(); + let mut s = state.borrow_mut(); + if let Some(conv) = + s.ui.dm_conversations + .iter() + .find(|c| c.other_nickname == nick) + { + s.ui.active_dm_id = Some(conv.dm_id.clone()); + s.ui.active_channel = None; + } + }); + } + + { + let state = state.clone(); + window.on_connect(move |addr| { + let addr = addr.to_string(); + let mut s = state.borrow_mut(); + s.ui.conn = ConnState::Connecting; + s.ui.connect_input = addr.clone(); + s.ui.status_msg = Some("connecting...".into()); + let net = s.net.clone(); + drop(s); + tokio::spawn(async move { + net.send(NetCommand::Connect { + address: addr, + auto_reconnect: true, + }) + .await; + }); + }); + } + + { + let state = state.clone(); + window.on_disconnect(move || { + let s = state.borrow(); + let net = s.net.clone(); + drop(s); + tokio::spawn(async move { + net.send(NetCommand::Disconnect).await; + }); + }); + } + + { + let state = state.clone(); + window.on_send_message(move |ch_id, text| { + let ch_id = ch_id.to_string(); + let text = text.to_string(); + if text.is_empty() { + return; + } + let mut s = state.borrow_mut(); + s.ui.chat_input = String::new(); + let net = s.net.clone(); + drop(s); + + if !ch_id.is_empty() { + tokio::spawn(async move { + net.send(NetCommand::SendChat { + channel_id: ch_id, + content: text, + reply_to: None, + }) + .await; + }); + } else { + let s = state.borrow(); + if let Some(dm_id) = s.ui.active_dm_id.clone() { + let net = s.net.clone(); + drop(s); + tokio::spawn(async move { + net.send(NetCommand::DmSend { + dm_id, + content: text, + }) + .await; + }); + } + } + }); + } + + { + let state = state.clone(); + window.on_add_friend(move |user_id| { + let user_id = user_id.to_string(); + let s = state.borrow(); + let net = s.net.clone(); + drop(s); + tokio::spawn(async move { + net.send(NetCommand::FriendRequest { + target_user_id: user_id, + }) + .await; + }); + }); + } + + { + let state = state.clone(); + window.on_accept_friend(move |user_id| { + let user_id = user_id.to_string(); + let s = state.borrow(); + let net = s.net.clone(); + drop(s); + tokio::spawn(async move { + net.send(NetCommand::FriendAccept { user_id }).await; + }); + }); + } + + { + let state = state.clone(); + window.on_decline_friend(move |user_id| { + let user_id = user_id.to_string(); + let s = state.borrow(); + let net = s.net.clone(); + drop(s); + tokio::spawn(async move { + net.send(NetCommand::FriendDecline { user_id }).await; + }); + }); + } + + { + let state = state.clone(); + window.on_remove_friend(move |user_id| { + let user_id = user_id.to_string(); + let s = state.borrow(); + let net = s.net.clone(); + drop(s); + tokio::spawn(async move { + net.send(NetCommand::FriendRemove { user_id }).await; + }); + }); + } + + { + let state = state.clone(); + window.on_start_dm(move |user_id| { + let user_id = user_id.to_string(); + let s = state.borrow(); + let net = s.net.clone(); + drop(s); + tokio::spawn(async move { + net.send(NetCommand::DmStart { + target_user_id: user_id, + }) + .await; + }); + }); + } + + { + let state = state.clone(); + window.on_toggle_mic(move || { + let mut s = state.borrow_mut(); + s.ui.mic_enabled = !s.ui.mic_enabled; + }); + } + + { + let state = state.clone(); + window.on_toggle_deafen(move || { + let mut s = state.borrow_mut(); + s.ui.deafen = !s.ui.deafen; + }); + } + + { + let state = state.clone(); + window.on_leave_voice(move || { + let mut s = state.borrow_mut(); + s.ui.active_channel = None; + s.voice_channel = None; + s.audio = None; + s.last_pipeline_config = None; + s.ui.voice = VoiceUiState::Off; + s.ui.voice_joined_at = None; + }); + } + + { + let state = state.clone(); + window.on_add_bookmark(move |_addr| { + state.borrow_mut().ui.add_bookmark_from_input(); + }); + } + + { + let state = state.clone(); + window.on_remove_bookmark(move |address| { + let addr = address.to_string(); + let mut s = state.borrow_mut(); + if let Some(idx) = s.ui.bookmarks.iter().position(|b| b.address == addr) { + s.ui.remove_bookmark(idx); + } + }); + } + + { + let state = state.clone(); + window.on_load_more(move |ch_id| { + let _ch_id = ch_id.to_string(); + let state = state.borrow(); + let net = state.net.clone(); + drop(state); + tokio::spawn(async move { + net.send(NetCommand::ChannelList).await; + }); + }); + } + + { + let state = state.clone(); + window.on_close_settings(move || { + state.borrow_mut().ui.settings_open = false; + }); + } + + { + let _state = state.clone(); + window.on_set_passphrase(move |_pass| {}); + } + + { + let _state = state.clone(); + window.on_export_keyfile(move || {}); + } + + { + let _state = state.clone(); + window.on_import_keyfile(move || {}); + } } diff --git a/src/net/internal/dispatch/dm.rs b/src/net/internal/dispatch/dm.rs index 127f709..c4e5be7 100644 --- a/src/net/internal/dispatch/dm.rs +++ b/src/net/internal/dispatch/dm.rs @@ -146,6 +146,41 @@ pub async fn handle(pid: u16, payload: &[u8], tx: &mpsc::Sender) -> Re _ => debug!("unhandled friend event: {}", p.event_type), } } + PID_E2EE_DM_KEY_EXCHANGE => { + let p: E2eeDmKeyExchangePayload = serde_json::from_slice(payload)?; + let _ = tx + .send(NetEvent::E2eeDmKeyExchange { + dm_id: p.dm_id, + e2ee_public_key: p.e2ee_public_key, + }) + .await; + } + PID_E2EE_DM_KEY_EXCHANGE_ACK => { + let p: E2eeDmKeyExchangeAckPayload = serde_json::from_slice(payload)?; + let _ = tx + .send(NetEvent::E2eeDmKeyExchangeAck { dm_id: p.dm_id }) + .await; + } + PID_E2EE_DM_MESSAGE => { + let p: E2eeDmMessagePayload = serde_json::from_slice(payload)?; + let _ = tx + .send(NetEvent::E2eeDmMessage { + dm_id: p.dm_id, + sender_id: p.sender_id, + ciphertext: p.ciphertext, + timestamp: p.timestamp, + }) + .await; + } + PID_E2EE_DM_HISTORY => { + let p: E2eeDmHistoryPayload = serde_json::from_slice(payload)?; + let _ = tx + .send(NetEvent::E2eeDmHistory { + dm_id: p.dm_id, + messages: p.messages, + }) + .await; + } _ => {} } Ok(()) diff --git a/src/net/internal/dispatch/mod.rs b/src/net/internal/dispatch/mod.rs index 2ebd8f2..4435209 100644 --- a/src/net/internal/dispatch/mod.rs +++ b/src/net/internal/dispatch/mod.rs @@ -30,7 +30,11 @@ pub async fn incoming(pid: u16, payload: &[u8], tx: &mpsc::Sender) -> | PID_MESSAGE_REACTION_REMOVE | PID_MESSAGE_EDIT | PID_MESSAGE_DELETE - | PID_FRIEND_EVENT => dm::handle(pid, payload, tx).await?, + | PID_FRIEND_EVENT + | PID_E2EE_DM_KEY_EXCHANGE + | PID_E2EE_DM_KEY_EXCHANGE_ACK + | PID_E2EE_DM_MESSAGE + | PID_E2EE_DM_HISTORY => dm::handle(pid, payload, tx).await?, PID_GUILD_LIST | PID_GUILD_CREATE diff --git a/src/net/payloads/dm.rs b/src/net/payloads/dm.rs index 16a7c48..fec8cc8 100644 --- a/src/net/payloads/dm.rs +++ b/src/net/payloads/dm.rs @@ -1,5 +1,34 @@ use serde::{Deserialize, Serialize}; +#[derive(Serialize, Deserialize, Debug)] +pub struct E2eeDmKeyExchangePayload { + pub dm_id: String, + pub e2ee_public_key: Vec, +} + +#[derive(Serialize, Deserialize, Debug)] +pub struct E2eeDmKeyExchangeAckPayload { + pub dm_id: String, +} + +#[derive(Serialize, Deserialize, Debug, Clone)] +pub struct E2eeDmMessagePayload { + pub dm_id: String, + pub sender_id: String, + #[serde(with = "serde_bytes")] + pub ciphertext: Vec, + pub timestamp: i64, +} + +#[derive(Serialize, Deserialize, Debug)] +pub struct E2eeDmHistoryPayload { + pub dm_id: String, + #[serde(default)] + pub messages: Vec, + #[serde(default)] + pub limit: Option, +} + #[derive(Serialize, Deserialize, Debug)] pub struct DmStartPayload { pub target_user_id: String, diff --git a/src/net/payloads/mod.rs b/src/net/payloads/mod.rs index 56d82f2..62a3644 100644 --- a/src/net/payloads/mod.rs +++ b/src/net/payloads/mod.rs @@ -26,6 +26,10 @@ pub const PID_MESSAGE_DELETE: u16 = 0x0069; pub const PID_READ_RECEIPT: u16 = 0x0064; pub const PID_READ_RECEIPT_BROADCAST: u16 = 0x0065; pub const PID_TYPING_START: u16 = 0x0070; +pub const PID_E2EE_DM_KEY_EXCHANGE: u16 = 0x0071; +pub const PID_E2EE_DM_KEY_EXCHANGE_ACK: u16 = 0x0072; +pub const PID_E2EE_DM_MESSAGE: u16 = 0x0073; +pub const PID_E2EE_DM_HISTORY: u16 = 0x0074; pub const PID_GUILD_CREATE: u16 = 0x0100; pub const PID_GUILD_DELETE: u16 = 0x0101; pub const PID_GUILD_LIST: u16 = 0x0102; diff --git a/src/net/session/handlers/social/mod.rs b/src/net/session/handlers/social/mod.rs index 2bb3063..7d5037f 100644 --- a/src/net/session/handlers/social/mod.rs +++ b/src/net/session/handlers/social/mod.rs @@ -140,6 +140,53 @@ pub async fn handle_cmd( })?; framing::io::write_encrypted(stream, PID_DM_HISTORY, seq, &payload, crypto).await?; } + NetCommand::E2eeDmKeyExchange { + dm_id, + e2ee_public_key, + } => { + let payload = serde_json::to_vec(&E2eeDmKeyExchangePayload { + dm_id, + e2ee_public_key, + })?; + framing::io::write_encrypted(stream, PID_E2EE_DM_KEY_EXCHANGE, seq, &payload, crypto) + .await?; + } + NetCommand::E2eeDmKeyExchangeAck { dm_id } => { + let payload = serde_json::to_vec(&E2eeDmKeyExchangeAckPayload { dm_id })?; + framing::io::write_encrypted( + stream, + PID_E2EE_DM_KEY_EXCHANGE_ACK, + seq, + &payload, + crypto, + ) + .await?; + } + NetCommand::E2eeDmSend { dm_id, ciphertext } => { + let msg = E2eeDmMessagePayload { + dm_id, + sender_id: identity.pubkey_hex.clone(), + ciphertext, + timestamp: now_ms(), + }; + framing::io::write_encrypted( + stream, + PID_E2EE_DM_MESSAGE, + seq, + &serde_json::to_vec(&msg)?, + crypto, + ) + .await?; + } + NetCommand::E2eeDmHistory { dm_id, limit } => { + let payload = serde_json::to_vec(&E2eeDmHistoryPayload { + dm_id, + messages: vec![], + limit, + })?; + framing::io::write_encrypted(stream, PID_E2EE_DM_HISTORY, seq, &payload, crypto) + .await?; + } _ => {} } Ok(()) diff --git a/src/net/types/command.rs b/src/net/types/command.rs index 29bef48..8d7d7ad 100644 --- a/src/net/types/command.rs +++ b/src/net/types/command.rs @@ -158,5 +158,20 @@ pub enum NetCommand { channel_id: String, message_id: String, }, + E2eeDmKeyExchange { + dm_id: String, + e2ee_public_key: Vec, + }, + E2eeDmKeyExchangeAck { + dm_id: String, + }, + E2eeDmSend { + dm_id: String, + ciphertext: Vec, + }, + E2eeDmHistory { + dm_id: String, + limit: Option, + }, Disconnect, } diff --git a/src/net/types/event.rs b/src/net/types/event.rs index 5fcb960..6a4bbcd 100644 --- a/src/net/types/event.rs +++ b/src/net/types/event.rs @@ -224,4 +224,21 @@ pub enum NetEvent { channel_id: String, message_id: String, }, + E2eeDmKeyExchange { + dm_id: String, + e2ee_public_key: Vec, + }, + E2eeDmKeyExchangeAck { + dm_id: String, + }, + E2eeDmMessage { + dm_id: String, + sender_id: String, + ciphertext: Vec, + timestamp: i64, + }, + E2eeDmHistory { + dm_id: String, + messages: Vec, + }, } diff --git a/src/ui/app/frame.rs b/src/ui/app/frame.rs deleted file mode 100644 index 4db52e2..0000000 --- a/src/ui/app/frame.rs +++ /dev/null @@ -1,51 +0,0 @@ -use eframe::egui::{self, Frame}; - -use crate::connect; -use crate::state::ConnState; -use crate::{chat, members, settings, sidebar, theme, titlebar}; - -use super::VnoxApp; - -impl eframe::App for VnoxApp { - fn update(&mut self, ctx: &egui::Context, _frame: &mut eframe::Frame) { - self.handle_voice_keybinds(ctx); - self.sync_audio_controls(ctx); - self.poll_net(); - - if matches!( - self.ui.conn, - ConnState::Connecting | ConnState::Reconnecting { .. } - ) { - ctx.request_repaint_after(std::time::Duration::from_millis(50)); - } - if matches!(self.ui.conn, ConnState::Connected { .. }) { - ctx.request_repaint_after(std::time::Duration::from_secs(1)); - } - if self.audio.is_some() { - ctx.request_repaint_after(std::time::Duration::from_millis(20)); - } - - titlebar::show_titlebar(ctx, &mut self.ui); - - if matches!(self.ui.conn, ConnState::Connected { .. }) { - sidebar::guild_bar(ctx, &mut self.ui, &self.net); - } - sidebar::show(ctx, &mut self.ui, &self.identity, &self.net); - if matches!(self.ui.conn, ConnState::Connected { .. }) { - members::show(ctx, &mut self.ui); - } - egui::CentralPanel::default() - .frame(Frame::NONE.fill(theme::BG_BASE)) - .show(ctx, |ui| match &self.ui.conn.clone() { - ConnState::Disconnected - | ConnState::Connecting - | ConnState::Reconnecting { .. } => { - connect::show(ui, &mut self.ui, &self.identity, &self.net) - } - ConnState::Connected { .. } => { - chat::show(ui, ctx, &mut self.ui, &self.identity, &self.net) - } - }); - settings::show_window(ctx, &mut self.ui, &self.identity); - } -} diff --git a/src/ui/app/mod.rs b/src/ui/app/mod.rs deleted file mode 100644 index e7a2dfa..0000000 --- a/src/ui/app/mod.rs +++ /dev/null @@ -1,123 +0,0 @@ -mod frame; -mod net_poll; -mod setup; - -use vnox_client::{ - audio::{AudioPipeline, AudioSync}, - identity::Identity, - net::{NetCommand, NetHandle}, -}; - -use crate::keybind; -use crate::state::{ConnState, VoiceUiState}; -use crate::theme::apply_visuals; - -pub struct VnoxApp { - pub(crate) identity: Identity, - pub(crate) net: NetHandle, - pub(crate) ui: crate::state::UiState, - pub(crate) audio: Option, - pub(crate) voice_channel: Option, - pub(crate) last_pipeline_config: Option, -} - -impl VnoxApp { - pub fn new(cc: &eframe::CreationContext, identity: Identity, net: NetHandle) -> Self { - apply_visuals(&cc.egui_ctx); - Self { - identity, - net, - ui: crate::state::UiState::default(), - audio: None, - voice_channel: None, - last_pipeline_config: None, - } - } - - pub(crate) fn stop_voice(&mut self) { - self.audio = None; - self.voice_channel = None; - self.last_pipeline_config = None; - self.ui.voice = VoiceUiState::Off; - self.ui.voice_joined_at = None; - } - - pub(crate) fn sync_voice_audio(&mut self) { - if !matches!(self.ui.conn, ConnState::Connected { .. }) { - self.stop_voice(); - return; - } - let active_voice = self - .ui - .active_channel - .as_ref() - .and_then(|id| self.ui.channels.iter().find(|c| &c.id == id)) - .filter(|c| c.kind == "voice") - .map(|c| c.id.clone()); - - match active_voice { - Some(id) => self.start_voice(&id), - None => self.stop_voice(), - } - } - - pub(crate) fn sync_audio_controls(&mut self, ctx: &eframe::egui::Context) { - let Some(pipeline) = self.audio.as_ref() else { - return; - }; - let ptt_held = if self.ui.vad_mode == 0 { - keybind::binding_held(ctx, &self.ui.kb_ptt) - } else { - false - }; - pipeline.sync_controls(AudioSync { - mic_enabled: self.ui.mic_enabled, - vad_mode: self.ui.vad_mode, - vad_threshold: self.ui.vad_threshold, - input_volume: self.ui.input_volume, - ptt_held, - output_volume: self.ui.output_volume, - jitter_size: self.ui.jitter_size, - adaptive_buffer: self.ui.adaptive_buffer, - voice_bitrate_kbps: self.ui.voice_bitrate, - noise_suppress: self.ui.noise_suppress, - }); - - if let Some(ch) = self.voice_channel.clone() { - let cfg = self.pipeline_config(&ch); - if self.last_pipeline_config.as_ref() != Some(&cfg) { - self.start_voice(&ch); - } - } - } - - pub(crate) fn handle_voice_keybinds(&mut self, ctx: &eframe::egui::Context) { - if ctx.wants_keyboard_input() { - return; - } - if keybind::binding_pressed(ctx, &self.ui.kb_mute) { - self.ui.mic_enabled = !self.ui.mic_enabled; - } - if keybind::binding_pressed(ctx, &self.ui.kb_deafen) { - self.ui.deafen = !self.ui.deafen; - } - } - - pub(crate) fn flush_encoded_voice(&mut self) { - let Some(pipeline) = self.audio.as_ref() else { - return; - }; - while let Some(frame) = pipeline.poll_encoded() { - let net = self.net.clone(); - tokio::spawn(async move { - net.send(NetCommand::SendVoice { - channel_id: frame.channel_id, - voice_seq: frame.voice_seq, - timestamp: frame.timestamp, - opus_data: frame.data, - }) - .await; - }); - } - } -} diff --git a/src/ui/app/net_poll/chat/channel.rs b/src/ui/app/net_poll/chat/channel.rs deleted file mode 100644 index 4c6b31a..0000000 --- a/src/ui/app/net_poll/chat/channel.rs +++ /dev/null @@ -1,138 +0,0 @@ -use crate::state::{Channel, DmConversation, UiState}; -use vnox_client::net::{NetCommand, NetEvent, NetHandle}; - -use super::into_msg; - -pub(super) fn handle(ui: &mut UiState, net: &NetHandle, event: NetEvent) { - match event { - NetEvent::ChannelState { - channel_id, - channel_name, - kind, - members, - .. - } => { - ui.server_channel = Some(channel_id.clone()); - let mems: Vec = members - .iter() - .map(|m| { - ui.user_names.insert(m.user_id.clone(), m.nickname.clone()); - m.nickname.clone() - }) - .collect(); - if let Some(ch) = ui.channels.iter_mut().find(|c| c.id == channel_id) { - ch.members = mems; - } else { - ui.channels.push(Channel { - id: channel_id.clone(), - name: channel_name, - kind: kind.clone(), - members: mems, - }); - } - if ui.active_channel.is_none() { - ui.active_channel = Some(channel_id.clone()); - } - let net = net.clone(); - let ch_id = channel_id; - tokio::spawn(async move { - net.send(NetCommand::ReadReceipt { - channel_id: ch_id, - last_read_message_id: uuid::Uuid::new_v4().to_string(), - }) - .await; - }); - } - NetEvent::UserJoin { - channel_id, - user_id, - nickname, - .. - } => { - ui.user_names.insert(user_id, nickname.clone()); - if let Some(ch) = ui.channels.iter_mut().find(|c| c.id == channel_id) - && !ch.members.contains(&nickname) - { - ch.members.push(nickname); - } - } - NetEvent::UserLeave { - channel_id, - user_id, - } => { - if let Some(ch) = ui.channels.iter_mut().find(|c| c.id == channel_id) { - let nickname = ui.user_names.remove(&user_id); - ch.members.retain(|m| { - m != &user_id && nickname.as_deref().map(|n| m != n).unwrap_or(true) - }); - } - } - NetEvent::ChannelCreated { - channel_id, - channel_name, - kind, - } => { - // Another session created a channel — add to local list if not present. - if !ui.channels.iter().any(|c| c.id == channel_id) { - ui.channels.push(Channel { - id: channel_id, - name: channel_name, - kind, - members: Vec::new(), - }); - } - } - NetEvent::ChannelDeleted { channel_id } => { - ui.channels.retain(|c| c.id != channel_id); - // Clear active_channel if it pointed at the deleted channel. - if ui.active_channel.as_deref() == Some(&channel_id) { - ui.active_channel = ui.channels.first().map(|c| c.id.clone()); - } - // Drop any cached messages for the deleted channel. - ui.messages.remove(&channel_id); - } - NetEvent::ChannelListEvent { channels } => { - // Replace local channel list with the server-authoritative one. - // Preserve `members` for channels we were already tracking. - let old: std::collections::HashMap = std::mem::take(&mut ui.channels) - .into_iter() - .map(|c| (c.id.clone(), c)) - .collect(); - ui.channels = channels - .into_iter() - .map(|c| { - let members = old - .get(&c.channel_id) - .map(|o| o.members.clone()) - .unwrap_or_default(); - Channel { - id: c.channel_id, - name: c.channel_name, - kind: c.kind, - members, - } - }) - .collect(); - } - NetEvent::DmStart { - dm_id, - other_user_id, - other_nickname, - messages, - unread_count, - } => { - if !ui.dm_conversations.iter().any(|c| c.dm_id == dm_id) { - ui.dm_conversations.push(DmConversation { - dm_id: dm_id.clone(), - other_user_id, - other_nickname, - unread_count, - }); - } - ui.dm_messages - .insert(dm_id.clone(), messages.into_iter().map(into_msg).collect()); - ui.active_dm_id = Some(dm_id); - } - _ => {} - } -} diff --git a/src/ui/app/net_poll/chat/messages.rs b/src/ui/app/net_poll/chat/messages.rs deleted file mode 100644 index 8d8fa52..0000000 --- a/src/ui/app/net_poll/chat/messages.rs +++ /dev/null @@ -1,149 +0,0 @@ -use crate::state::{ChatMessage, UiState}; -use std::collections::HashMap; -use vnox_client::net::{NetEvent, NetHandle}; - -use super::into_msg; - -pub(super) fn handle(ui: &mut UiState, _net: &NetHandle, event: NetEvent) { - match event { - NetEvent::ChatMessage { - message_id, - channel_id, - sender_id, - content, - timestamp, - reply_to, - .. - } => { - ui.messages - .entry(channel_id) - .or_default() - .push(ChatMessage { - message_id, - sender_id, - content, - timestamp, - edited: false, - reactions: HashMap::new(), - reply_to, - }); - ui.scroll_bottom = true; - } - NetEvent::ChatHistory { - channel_id, - messages, - } => { - ui.messages - .insert(channel_id, messages.into_iter().map(into_msg).collect()); - ui.scroll_bottom = true; - } - NetEvent::Error { message, .. } => { - ui.status_msg = Some(format!("error: {message}")); - } - NetEvent::ReactionAdded { - channel_id, - message_id, - user_id, - emoji, - } => { - if let Some(msgs) = ui.messages.get_mut(&channel_id) - && let Some(msg) = msgs.iter_mut().find(|m| m.message_id == message_id) - { - msg.reactions.entry(emoji).or_default().push(user_id); - } - } - NetEvent::ReactionRemoved { - channel_id, - message_id, - user_id, - emoji, - } => { - if let Some(msgs) = ui.messages.get_mut(&channel_id) - && let Some(msg) = msgs.iter_mut().find(|m| m.message_id == message_id) - && let Some(users) = msg.reactions.get_mut(&emoji) - { - users.retain(|u| u != &user_id); - if users.is_empty() { - msg.reactions.remove(&emoji); - } - } - } - NetEvent::MessageEdited { - channel_id, - message_id, - content, - .. - } => { - if let Some(msgs) = ui.messages.get_mut(&channel_id) - && let Some(msg) = msgs.iter_mut().find(|m| m.message_id == message_id) - { - msg.content = content; - msg.edited = true; - } - } - NetEvent::MessageDeleted { - channel_id, - message_id, - } => { - if let Some(msgs) = ui.messages.get_mut(&channel_id) { - msgs.retain(|m| m.message_id != message_id); - } - } - NetEvent::DmMessage { - dm_id, - sender_id, - content, - timestamp, - .. - } => { - ui.dm_messages - .entry(dm_id.clone()) - .or_default() - .push(ChatMessage { - message_id: uuid::Uuid::new_v4().to_string(), - sender_id, - content, - timestamp, - edited: false, - reactions: HashMap::new(), - reply_to: None, - }); - if ui.active_dm_id.as_deref() != Some(&dm_id) - && let Some(c) = ui.dm_conversations.iter_mut().find(|c| c.dm_id == dm_id) - { - c.unread_count += 1; - } - } - NetEvent::DmHistory { dm_id, messages } => { - ui.dm_messages - .insert(dm_id, messages.into_iter().map(into_msg).collect()); - } - NetEvent::ReadReceiptBroadcast { - channel_id, - user_id, - last_read_message_id, - } => { - ui.read_receipts - .entry(channel_id) - .or_default() - .insert(user_id, last_read_message_id); - } - NetEvent::UserColor { user_id, color } => { - if let Some(c) = parse_hex_color(&color) { - ui.user_colors.insert(user_id, c); - } - } - _ => {} - } -} - -fn parse_hex_color(hex: &str) -> Option { - let hex = hex.trim_start_matches('#'); - if hex.len() != 6 { - return None; - } - let r = u8::from_str_radix(&hex[0..2], 16).ok()?; - let g = u8::from_str_radix(&hex[2..4], 16).ok()?; - let b = u8::from_str_radix(&hex[4..6], 16).ok()?; - Some(eframe::egui::Color32::from_rgb(r, g, b)) -} diff --git a/src/ui/app/net_poll/chat/mod.rs b/src/ui/app/net_poll/chat/mod.rs deleted file mode 100644 index 1fb37b9..0000000 --- a/src/ui/app/net_poll/chat/mod.rs +++ /dev/null @@ -1,28 +0,0 @@ -mod channel; -mod messages; - -use crate::state::{ChatMessage, UiState}; -use std::collections::HashMap; -use vnox_client::net::{ChatMsg, NetEvent, NetHandle}; - -fn into_msg(m: ChatMsg) -> ChatMessage { - ChatMessage { - message_id: m.message_id, - sender_id: m.sender_id, - content: m.content, - timestamp: m.timestamp, - edited: false, - reactions: HashMap::new(), - reply_to: m.reply_to, - } -} - -pub(crate) fn handle(ui: &mut UiState, net: &NetHandle, event: NetEvent) { - match event { - NetEvent::ChannelState { .. } - | NetEvent::UserJoin { .. } - | NetEvent::UserLeave { .. } - | NetEvent::DmStart { .. } => channel::handle(ui, net, event), - e => messages::handle(ui, net, e), - } -} diff --git a/src/ui/app/net_poll/mod.rs b/src/ui/app/net_poll/mod.rs deleted file mode 100644 index 5ec1d4a..0000000 --- a/src/ui/app/net_poll/mod.rs +++ /dev/null @@ -1,169 +0,0 @@ -mod chat; -mod social; -mod status; - -use vnox_client::{ - audio::IncomingVoice, - net::{NetCommand, NetEvent, NetHandle}, -}; - -use crate::state::ConnState; - -use super::VnoxApp; - -impl VnoxApp { - pub(crate) fn poll_net(&mut self) { - while let Some(ev) = self.net.try_recv() { - match ev { - NetEvent::Connected { - session_id, - node_name, - restored, - private_mode, - } => self.on_connected(session_id, node_name, restored, private_mode), - NetEvent::Disconnected { reason, will_retry } => { - self.stop_voice(); - status::handle_disconnected(&mut self.ui, &reason, will_retry); - } - NetEvent::Reconnecting { - attempt, - delay_secs, - } => { - self.stop_voice(); - status::handle_reconnecting(&mut self.ui, attempt, delay_secs); - } - NetEvent::VoicePacket { - channel_id, - voice_seq, - timestamp, - opus_data, - sender_id, - .. - } => { - // Track which remote user is speaking (Phase 1.3 attribution). - if !sender_id.is_empty() { - self.ui.last_remote_speaker_id = sender_id.clone(); - self.ui.last_voice_packet_ms = std::time::SystemTime::now() - .duration_since(std::time::UNIX_EPOCH) - .map(|d| d.as_millis() as u64) - .unwrap_or(0); - } - if !self.ui.deafen - && let Some(pipeline) = self.audio.as_ref() - { - pipeline.push_voice(IncomingVoice { - voice_seq, - timestamp, - channel_id, - opus_data, - }); - } - } - event => process_net_event(&mut self.ui, &self.net, event), - } - self.sync_voice_audio(); - self.flush_encoded_voice(); - } - // Refresh speaking indicators from the audio pipeline. - if let Some(pipeline) = self.audio.as_ref() { - self.ui.local_speaking = pipeline.local_speaking(); - self.ui.remote_speaking = pipeline.remote_speaking(500); - } else { - self.ui.local_speaking = false; - self.ui.remote_speaking = false; - } - // Decay remote speaker id if no packet arrived in 700 ms. - if !self.ui.remote_speaking { - self.ui.last_remote_speaker_id.clear(); - } - } - - pub(crate) fn on_connected( - &mut self, - session_id: String, - node_name: String, - restored: bool, - private_mode: bool, - ) { - self.ui.conn = ConnState::Connected { - session_id, - node_name, - }; - self.ui.private_mode = private_mode; - if restored { - self.ui.status_msg = Some("reconnected".into()); - if let Some(ch) = self.ui.active_channel.clone() { - let net = self.net.clone(); - tokio::spawn(async move { - net.send(NetCommand::JoinChannel { channel_id: ch }).await; - }); - } - } else { - self.ui.seed_default_channels(); - self.ui.active_channel = Some("general".into()); - self.ui.status_msg = None; - let net = self.net.clone(); - tokio::spawn(async move { - net.send(NetCommand::JoinChannel { - channel_id: "general".into(), - }) - .await; - }); - } - } -} - -pub(crate) fn process_net_event(ui: &mut crate::state::UiState, net: &NetHandle, event: NetEvent) { - match &event { - NetEvent::ChannelState { .. } - | NetEvent::ChannelCreated { .. } - | NetEvent::ChannelDeleted { .. } - | NetEvent::ChannelListEvent { .. } - | NetEvent::ChatMessage { .. } - | NetEvent::ChatHistory { .. } - | NetEvent::UserJoin { .. } - | NetEvent::UserLeave { .. } - | NetEvent::Error { .. } - | NetEvent::ReactionAdded { .. } - | NetEvent::ReactionRemoved { .. } - | NetEvent::MessageEdited { .. } - | NetEvent::MessageDeleted { .. } - | NetEvent::DmStart { .. } - | NetEvent::DmMessage { .. } - | NetEvent::DmHistory { .. } - | NetEvent::ReadReceiptBroadcast { .. } - | NetEvent::UserColor { .. } => chat::handle(ui, net, event), - - NetEvent::GuildList { .. } - | NetEvent::GuildCreated { .. } - | NetEvent::GuildDeleted { .. } - | NetEvent::GuildMemberJoined { .. } - | NetEvent::GuildMemberLeft { .. } - | NetEvent::GuildMemberKicked { .. } - | NetEvent::InviteCreated { .. } - | NetEvent::InviteAccepted { .. } - | NetEvent::InviteDeleted { .. } - | NetEvent::RoleCreated { .. } - | NetEvent::RoleDeleted { .. } - | NetEvent::GuildAuditLog { .. } - | NetEvent::GuildMemberList { .. } - | NetEvent::GuildRoleList { .. } - | NetEvent::FriendList { .. } - | NetEvent::FriendRequested { .. } - | NetEvent::FriendAccepted { .. } - | NetEvent::FriendRemoved { .. } - | NetEvent::BlockList { .. } - | NetEvent::BlockedUser { .. } - | NetEvent::UnblockedUser { .. } => social::handle(ui, event), - - NetEvent::LatencyUpdate { .. } - | NetEvent::TypingStart { .. } - | NetEvent::PresenceSync { .. } - | NetEvent::PresenceUpdated { .. } => status::handle(ui, net, event), - - NetEvent::Connected { .. } - | NetEvent::Disconnected { .. } - | NetEvent::Reconnecting { .. } - | NetEvent::VoicePacket { .. } => {} - } -} diff --git a/src/ui/app/net_poll/social.rs b/src/ui/app/net_poll/social.rs deleted file mode 100644 index 2789002..0000000 --- a/src/ui/app/net_poll/social.rs +++ /dev/null @@ -1,148 +0,0 @@ -use vnox_client::net::NetEvent; - -use crate::state::{FriendState, GuildState}; - -use crate::state::UiState; - -pub(crate) fn handle(ui: &mut UiState, event: NetEvent) { - match event { - NetEvent::GuildAuditLog { guild_id, entries } => { - ui.audit_log_entries = entries; - ui.audit_log_guild_id = Some(guild_id); - } - NetEvent::GuildMemberList { guild_id, members } => { - ui.guild_member_list_guild_id = Some(guild_id); - ui.guild_member_list = members; - } - NetEvent::GuildRoleList { guild_id, roles } => { - ui.guild_role_list_guild_id = Some(guild_id); - ui.guild_role_list = roles; - } - NetEvent::BlockList { blocked } => { - ui.blocked_users = blocked; - } - NetEvent::BlockedUser { user_id } => { - if !ui.blocked_users.contains(&user_id) { - ui.blocked_users.push(user_id); - } - } - NetEvent::UnblockedUser { user_id } => { - ui.blocked_users.retain(|u| u != &user_id); - } - NetEvent::GuildList { guilds } => { - ui.guilds = guilds - .into_iter() - .map(|g| GuildState { - guild_id: g.guild_id, - name: g.name, - owner_id: g.owner_id, - member_count: g.member_count, - }) - .collect(); - } - NetEvent::GuildCreated { - guild_id, - name, - owner_id, - } => { - ui.guilds.push(GuildState { - guild_id: guild_id.clone(), - name, - owner_id, - member_count: 1, - }); - ui.active_guild_id = Some(guild_id); - } - NetEvent::GuildDeleted { guild_id } => { - ui.guilds.retain(|g| g.guild_id != guild_id); - if ui.active_guild_id.as_deref() == Some(&guild_id) { - ui.active_guild_id = None; - } - } - NetEvent::GuildMemberJoined { - guild_id, - user_id: _, - nickname: _, - } => { - if let Some(g) = ui.guilds.iter_mut().find(|g| g.guild_id == guild_id) { - g.member_count += 1; - } - } - NetEvent::GuildMemberLeft { - guild_id, - user_id: _, - } => { - if let Some(g) = ui.guilds.iter_mut().find(|g| g.guild_id == guild_id) { - g.member_count = g.member_count.saturating_sub(1); - } - } - NetEvent::GuildMemberKicked { - guild_id, - user_id: _, - } => { - if let Some(g) = ui.guilds.iter_mut().find(|g| g.guild_id == guild_id) { - g.member_count = g.member_count.saturating_sub(1); - } - } - NetEvent::InviteCreated { .. } => { - // UI will fetch invite list separately if needed - } - NetEvent::InviteAccepted { - guild_id, - guild_name, - } if !ui.guilds.iter().any(|g| g.guild_id == guild_id) => { - ui.guilds.push(GuildState { - guild_id, - name: guild_name, - owner_id: String::new(), - member_count: 1, - }); - } - NetEvent::InviteDeleted { - guild_id: _, - invite_id: _, - } => {} - NetEvent::RoleCreated { .. } => {} - NetEvent::RoleDeleted { .. } => {} - NetEvent::FriendList { friends } => { - ui.friends = friends - .into_iter() - .map(|f| FriendState { - user_id: f.user_id, - nickname: f.nickname, - status: f.status, - since: f.since, - }) - .collect(); - } - NetEvent::FriendRequested { user_id, nickname } => { - ui.pending_friend_requests.push(FriendState { - user_id, - nickname, - status: "offline".into(), - since: 0, - }); - } - NetEvent::FriendAccepted { user_id, nickname } => { - // Remove from pending requests - ui.pending_friend_requests.retain(|f| f.user_id != user_id); - // Avoid duplicate entries in friends list - if !ui.friends.iter().any(|f| f.user_id == user_id) { - ui.friends.push(FriendState { - user_id, - nickname, - status: "online".into(), - since: std::time::SystemTime::now() - .duration_since(std::time::UNIX_EPOCH) - .map(|d| d.as_millis() as i64) - .unwrap_or(0), - }); - } - } - NetEvent::FriendRemoved { user_id } => { - ui.friends.retain(|f| f.user_id != user_id); - ui.pending_friend_requests.retain(|f| f.user_id != user_id); - } - _ => {} - } -} diff --git a/src/ui/app/net_poll/status.rs b/src/ui/app/net_poll/status.rs deleted file mode 100644 index 3e17e8c..0000000 --- a/src/ui/app/net_poll/status.rs +++ /dev/null @@ -1,69 +0,0 @@ -use vnox_client::net::{NetEvent, NetHandle, PresenceInfo}; - -use crate::state::ConnState; - -use crate::state::UiState; - -pub(crate) fn handle_disconnected(ui: &mut UiState, reason: &str, will_retry: bool) { - if will_retry { - ui.status_msg = Some(format!("connection lost: {reason}")); - } else { - ui.conn = ConnState::Disconnected; - ui.channels.clear(); - ui.active_channel = None; - ui.user_names.clear(); - ui.server_channel = None; - ui.rtt_ms = None; - ui.status_msg = Some(format!("disconnected: {reason}")); - } - ui.dm_conversations.clear(); - ui.dm_messages.clear(); - ui.active_dm_id = None; - ui.new_dm_input = None; - ui.private_mode = false; -} - -pub(crate) fn handle_reconnecting(ui: &mut UiState, attempt: u32, delay_secs: u64) { - ui.conn = ConnState::Reconnecting { attempt }; - ui.status_msg = Some(format!("reconnecting in {delay_secs}s (attempt {attempt})")); -} - -pub(crate) fn handle(ui: &mut UiState, _net: &NetHandle, event: NetEvent) { - match event { - NetEvent::LatencyUpdate { rtt_ms } => { - ui.rtt_ms = Some(rtt_ms); - } - NetEvent::TypingStart { - user_id, - nickname, - channel_id, - } => { - let entry = ui.typing_users.entry(channel_id).or_default(); - entry.retain(|(u, _)| u != &user_id); - entry.push((nickname, std::time::Instant::now())); - } - NetEvent::PresenceSync { presences } => { - for p in presences { - ui.presences.insert(p.user_id.clone(), p); - } - } - NetEvent::PresenceUpdated { - user_id, - status, - custom_status, - activity, - } => { - ui.presences.insert( - user_id.clone(), - PresenceInfo { - user_id, - status, - custom_status, - activity, - activity_text: None, - }, - ); - } - _ => {} - } -} diff --git a/src/ui/app/setup.rs b/src/ui/app/setup.rs deleted file mode 100644 index 8941b4b..0000000 --- a/src/ui/app/setup.rs +++ /dev/null @@ -1,50 +0,0 @@ -use tracing::{info, warn}; -use vnox_client::audio::{self, PipelineConfig}; -use vnox_client::net::voice; - -use super::VnoxApp; - -impl VnoxApp { - pub(crate) fn pipeline_config(&self, channel_id: &str) -> PipelineConfig { - PipelineConfig { - channel_key: voice::channel_key(channel_id), - mic_device_index: self.ui.mic_device, - output_device_index: self.ui.output_device, - voice_bitrate_kbps: self.ui.voice_bitrate, - packet_interval: self.ui.packet_interval, - } - } - - pub(crate) fn start_voice(&mut self, channel_id: &str) { - let cfg = self.pipeline_config(channel_id); - if self.voice_channel.as_deref() == Some(channel_id) - && self.audio.is_some() - && self.last_pipeline_config.as_ref() == Some(&cfg) - { - return; - } - self.audio = None; - match audio::start(cfg.clone()) { - Ok(pipeline) => { - info!("voice active on channel {channel_id}"); - self.audio = Some(pipeline); - self.voice_channel = Some(channel_id.to_string()); - self.last_pipeline_config = Some(cfg); - self.ui.voice = crate::state::VoiceUiState::Active { - channel: channel_id.to_string(), - }; - if self.ui.voice_joined_at.is_none() { - self.ui.voice_joined_at = Some(std::time::Instant::now()); - } - self.ui.status_msg = None; - } - Err(e) => { - warn!("audio start failed: {e}"); - self.voice_channel = None; - self.last_pipeline_config = None; - self.ui.voice = crate::state::VoiceUiState::Error(e.to_string()); - self.ui.status_msg = Some(format!("microphone error: {e}")); - } - } - } -} diff --git a/src/ui/bridge.rs b/src/ui/bridge.rs new file mode 100644 index 0000000..69981ce --- /dev/null +++ b/src/ui/bridge.rs @@ -0,0 +1,267 @@ +use std::rc::Rc; + +use slint::{SharedString, VecModel}; + +use crate::ui::state::{ConnState, UiState}; + +type MainWindow = crate::ui::slint::MainWindow; +type BookmarkItem = crate::ui::slint::BookmarkItem; +type ChannelItem = crate::ui::slint::ChannelItem; +type GuildItem = crate::ui::slint::GuildItem; +type MessageItem = crate::ui::slint::MessageItem; +type MemberItem = crate::ui::slint::MemberItem; +type FriendItem = crate::ui::slint::FriendItem; +type VoiceMemberItem = crate::ui::slint::VoiceMemberItem; + +fn fmt_ts(ts: i64) -> String { + let secs = (ts / 1000).abs() % 86400; + format!("{:02}:{:02}", secs / 3600, (secs % 3600) / 60) +} + +fn set_messages(window: &MainWindow, msgs: &[crate::ui::state::ChatMessage], own_user_id: &str) { + let items: Vec = msgs + .iter() + .map(|m| MessageItem { + message_id: SharedString::from(m.message_id.as_str()), + sender_id: SharedString::from(m.sender_id.as_str()), + sender_name: SharedString::from(m.sender_id.as_str()), + content: SharedString::from(m.content.as_str()), + timestamp: SharedString::from(fmt_ts(m.timestamp)), + is_own: m.sender_id == own_user_id, + edited: m.edited, + reply_to: SharedString::from(m.reply_to.as_deref().unwrap_or("")), + }) + .collect(); + window.set_messages(Rc::new(VecModel::from(items)).into()); +} + +fn set_members_state(window: &MainWindow, state: &UiState, ch_id: &str) { + if let Some(ch) = state.channels.iter().find(|c| c.id == ch_id) { + let items: Vec = ch + .members + .iter() + .map(|nick| { + let user_id = state + .user_names + .iter() + .find(|(_, n)| *n == nick) + .map(|(id, _)| id.clone()) + .unwrap_or_else(|| nick.clone()); + let status = state + .presences + .get(&user_id) + .map(|p| p.status.as_str()) + .unwrap_or("offline"); + MemberItem { + user_id: SharedString::from(user_id), + nickname: SharedString::from(nick.as_str()), + status: SharedString::from(status), + } + }) + .collect(); + window.set_members(Rc::new(VecModel::from(items)).into()); + } +} + +fn sync_sidebar(window: &MainWindow, state: &UiState) { + let channels: Vec = state + .channels + .iter() + .map(|c| ChannelItem { + channel_id: SharedString::from(c.id.as_str()), + name: SharedString::from(c.name.as_str()), + kind: SharedString::from(c.kind.as_str()), + }) + .collect(); + window.set_channels(Rc::new(VecModel::from(channels)).into()); + + let guilds: Vec = state + .guilds + .iter() + .map(|g| GuildItem { + guild_id: SharedString::from(g.guild_id.as_str()), + name: SharedString::from(g.name.as_str()), + }) + .collect(); + window.set_guilds(Rc::new(VecModel::from(guilds)).into()); + + let dms: Vec = state + .dm_conversations + .iter() + .map(|c| SharedString::from(c.other_nickname.as_str())) + .collect(); + window.set_dm_conversations(Rc::new(VecModel::from(dms)).into()); + + window.set_username(SharedString::from("user")); + window.set_user_status(SharedString::from("online")); + window.set_active_channel_id(SharedString::from( + state.active_channel.as_deref().unwrap_or(""), + )); + window.set_active_guild_idx(state.active_guild_id.clone().map_or(-1, |id| { + state + .guilds + .iter() + .position(|g| g.guild_id == id) + .map_or(-1, |i| i as i32) + })); +} + +fn sync_chat(window: &MainWindow, state: &UiState, own_user_id: &str) { + if let Some(ch_id) = &state.active_channel { + if let Some(ch) = state.channels.iter().find(|c| &c.id == ch_id) { + window.set_active_channel_name(SharedString::from(ch.name.as_str())); + if ch.kind == "voice" { + window.set_show_chat(false); + window.set_show_voice(true); + window.set_show_friends(false); + } else { + window.set_show_chat(true); + window.set_show_voice(false); + window.set_show_friends(false); + if let Some(msgs) = state.messages.get(ch_id) { + set_messages(window, msgs, own_user_id); + } else { + window.set_messages(Rc::new(VecModel::from(vec![])).into()); + } + set_members_state(window, state, ch_id); + } + } + } else { + window.set_show_chat(false); + window.set_show_voice(false); + window.set_show_friends(true); + } + window.set_input_text(SharedString::from(state.chat_input.as_str())); +} + +fn sync_friends(window: &MainWindow, state: &UiState) { + let friends: Vec = state + .friends + .iter() + .map(|f| FriendItem { + user_id: SharedString::from(f.user_id.as_str()), + nickname: SharedString::from(f.nickname.as_str()), + status: SharedString::from( + state + .presences + .get(&f.user_id) + .map(|p| p.status.as_str()) + .unwrap_or("offline"), + ), + }) + .collect(); + window.set_friends(Rc::new(VecModel::from(friends)).into()); + + let pending: Vec = state + .pending_friend_requests + .iter() + .map(|f| FriendItem { + user_id: SharedString::from(f.user_id.as_str()), + nickname: SharedString::from(f.nickname.as_str()), + status: SharedString::from("pending"), + }) + .collect(); + window.set_pending_requests(Rc::new(VecModel::from(pending)).into()); + + let blocked: Vec = state + .blocked_users + .iter() + .map(|u| SharedString::from(u.as_str())) + .collect(); + window.set_blocked_users(Rc::new(VecModel::from(blocked)).into()); + + window.set_friends_active_tab(SharedString::from(state.friends_tab.as_str())); +} + +pub fn sync_voice(window: &MainWindow, state: &UiState) { + let members: Vec = state + .channels + .iter() + .filter(|c| { + matches!( + &state.voice, + crate::ui::state::VoiceUiState::Active { channel } if channel == &c.id + ) + }) + .flat_map(|c| c.members.iter()) + .map(|nick| { + let user_id = state + .user_names + .iter() + .find(|(_, n)| *n == nick) + .map(|(id, _)| id.clone()) + .unwrap_or_else(|| nick.clone()); + VoiceMemberItem { + user_id: SharedString::from(user_id.clone()), + nickname: SharedString::from(nick.as_str()), + speaking: state.last_remote_speaker_id == user_id, + } + }) + .collect(); + window.set_voice_members(Rc::new(VecModel::from(members)).into()); + window.set_mic_enabled(state.mic_enabled); + window.set_deafened(state.deafen); + + if let crate::ui::state::VoiceUiState::Active { channel } = &state.voice { + if let Some(ch) = state.channels.iter().find(|c| &c.id == channel) { + window.set_voice_channel_name(SharedString::from(ch.name.as_str())); + } + } + + let banner = match (state.local_speaking, state.remote_speaking) { + (true, true) => "you and others are talking".to_string(), + (true, false) => "you are talking".to_string(), + (false, true) => format!( + "{} is talking", + state.nick_for(&state.last_remote_speaker_id) + ), + _ => String::new(), + }; + window.set_activity_banner(SharedString::from(banner)); +} + +pub fn sync_connect(window: &MainWindow, state: &UiState) { + let bookmarks: Vec = state + .bookmarks + .iter() + .map(|bm| BookmarkItem { + name: SharedString::from(bm.label.as_str()), + address: SharedString::from(bm.address.as_str()), + }) + .collect(); + window.set_bookmarks(Rc::new(VecModel::from(bookmarks)).into()); + window.set_address_input(SharedString::from(state.connect_input.as_str())); + window.set_status_text(SharedString::from( + state.status_msg.as_deref().unwrap_or(""), + )); + window.set_connecting(matches!( + state.conn, + ConnState::Connecting | ConnState::Reconnecting { .. } + )); + window.set_auto_reconnect(state.auto_reconnect); + if let ConnState::Connected { ref node_name, .. } = state.conn { + window.set_node_name(SharedString::from(node_name.as_str())); + } else { + window.set_node_name(SharedString::from("")); + } +} + +pub fn sync_settings(window: &MainWindow, state: &UiState) { + window.set_identity_pubkey(SharedString::from("")); + window.set_vault_enabled(false); + window.set_voice_input_device(SharedString::from("")); + window.set_voice_output_device(SharedString::from("")); + window.set_voice_bitrate(state.voice_bitrate); + window.set_vad_mode(state.vad_mode as i32); + window.set_volume(state.output_volume); + window.set_active_tab(SharedString::from("audio")); +} + +pub fn sync_ui(window: &MainWindow, state: &UiState, own_user_id: &str) { + sync_sidebar(window, state); + sync_chat(window, state, own_user_id); + sync_friends(window, state); + sync_voice(window, state); + sync_connect(window, state); + sync_settings(window, state); +} diff --git a/src/ui/chat/dm/messages.rs b/src/ui/chat/dm/messages.rs deleted file mode 100644 index 59b96b6..0000000 --- a/src/ui/chat/dm/messages.rs +++ /dev/null @@ -1,165 +0,0 @@ -use super::super::text::message_row; -use crate::state::{ChatMessage, UiState}; -use crate::theme as t; -use eframe::egui::{self, Context, FontId, Frame, RichText, ScrollArea, TextEdit}; -use std::collections::HashMap; -use vnox_client::{ - identity::Identity, - net::{NetCommand, NetHandle}, -}; - -pub(super) fn search_bar( - ui: &mut egui::Ui, - s: &mut UiState, - net: &NetHandle, - dm_id: &str, - _h: f32, -) { - Frame::NONE - .inner_margin(egui::Margin::symmetric(12, 4)) - .show(ui, |ui| { - ui.horizontal(|ui| { - let resp = ui.add_sized( - egui::vec2(ui.available_width(), 20.0), - TextEdit::singleline(&mut s.dm_search_query) - .font(FontId::proportional(12.0)) - .text_color(t::TEXT_PRIMARY) - .hint_text(RichText::new("search…").color(t::TEXT_DISABLED)) - .frame(true), - ); - if resp.changed() { - let query = s.dm_search_query.trim().to_string(); - let net = net.clone(); - let id = dm_id.to_string(); - tokio::spawn(async move { - net.send(NetCommand::DmSearch { dm_id: id, query }).await; - }); - } - }); - }); -} - -pub(super) fn show( - ui: &mut egui::Ui, - s: &mut UiState, - net: &NetHandle, - identity: &Identity, - dm_id: &str, - max_h: f32, -) { - let msgs = s.dm_messages.get(dm_id).cloned().unwrap_or_default(); - ScrollArea::vertical() - .max_height(max_h) - .auto_shrink([false; 2]) - .stick_to_bottom(s.scroll_bottom) - .show(ui, |ui| { - ui.add_space(12.0); - if msgs.is_empty() { - ui.label( - RichText::new("no messages yet") - .font(FontId::proportional(12.0)) - .color(t::TEXT_DISABLED), - ); - } else { - ui.label( - RichText::new("today") - .font(FontId::proportional(11.0)) - .color(t::TEXT_DISABLED), - ); - ui.add_space(8.0); - let mut prev: Option = None; - for msg in &msgs { - let group_start = prev.as_deref() != Some(&msg.sender_id); - message_row(ui, s, net, identity, msg, dm_id, group_start); - prev = Some(msg.sender_id.clone()); - } - } - ui.add_space(8.0); - }); - s.scroll_bottom = false; -} - -pub(super) fn input_bar( - ui: &mut egui::Ui, - ctx: &Context, - s: &mut UiState, - net: &NetHandle, - _identity: &Identity, - dm_id: &str, -) { - let r = ui.available_rect_before_wrap(); - ui.painter().hline( - r.x_range(), - r.top(), - egui::Stroke::new(1.0, t::BORDER_SUBTLE), - ); - - Frame::NONE - .fill(t::BG_SURFACE) - .inner_margin(egui::Margin::symmetric(12, 8)) - .show(ui, |ui| { - ui.horizontal(|ui| { - let input = egui::TextEdit::singleline(&mut s.chat_input) - .font(FontId::proportional(13.0)) - .text_color(t::TEXT_PRIMARY) - .hint_text(RichText::new("message…").color(t::TEXT_DISABLED)) - .frame(false) - .desired_width(ui.available_width() - 44.0); - - let enter = Frame::NONE - .fill(t::BG_INTERACTIVE) - .corner_radius(t::R_MD) - .stroke(egui::Stroke::new(1.0, t::BORDER_DEFAULT)) - .inner_margin(egui::Margin::symmetric(12, 6)) - .show(ui, |ui| ui.add(input)) - .inner; - - let submit = (enter.lost_focus() && ui.input(|i| i.key_pressed(egui::Key::Enter))) - || ui - .add( - egui::Button::new(RichText::new("→").color(t::ACCENT).size(14.0)) - .fill(t::ACCENT_10) - .stroke(egui::Stroke::new(1.0, t::BORDER_ACCENT)) - .corner_radius(t::R_MD) - .min_size(egui::vec2(32.0, 32.0)), - ) - .clicked(); - - if submit && !s.chat_input.trim().is_empty() { - send_dm(s, net, dm_id); - ctx.request_repaint(); - } - }); - }); -} - -fn send_dm(s: &mut UiState, net: &NetHandle, dm_id: &str) { - let content = s.chat_input.trim().to_string(); - if content.is_empty() { - return; - } - s.chat_input.clear(); - - s.dm_messages - .entry(dm_id.to_string()) - .or_default() - .push(ChatMessage { - message_id: uuid::Uuid::new_v4().to_string(), - sender_id: String::new(), - content: content.clone(), - timestamp: std::time::SystemTime::now() - .duration_since(std::time::UNIX_EPOCH) - .unwrap_or_default() - .as_millis() as i64, - edited: false, - reactions: HashMap::new(), - reply_to: None, - }); - s.scroll_bottom = true; - - let net = net.clone(); - let id = dm_id.to_string(); - tokio::spawn(async move { - net.send(NetCommand::DmSend { dm_id: id, content }).await; - }); -} diff --git a/src/ui/chat/dm/mod.rs b/src/ui/chat/dm/mod.rs deleted file mode 100644 index 4286921..0000000 --- a/src/ui/chat/dm/mod.rs +++ /dev/null @@ -1,71 +0,0 @@ -mod messages; - -use eframe::egui::{self, Context, FontId, Frame, RichText}; -use vnox_client::{identity::Identity, net::NetHandle}; - -use crate::state::UiState; -use crate::theme as t; - -pub(crate) fn dm_chat( - ui: &mut egui::Ui, - ctx: &Context, - s: &mut UiState, - identity: &Identity, - net: &NetHandle, - dm_id: &str, -) { - let conv = s - .dm_conversations - .iter() - .find(|c| c.dm_id == dm_id) - .cloned(); - - ui.with_layout(egui::Layout::top_down_justified(egui::Align::LEFT), |ui| { - Frame::NONE - .fill(t::BG_SURFACE) - .inner_margin(egui::Margin::symmetric(16, 0)) - .show(ui, |ui| { - ui.set_height(40.0); - ui.horizontal_centered(|ui| { - ui.label(RichText::new("@").color(t::TEXT_MUTED).size(16.0)); - ui.add_space(4.0); - let name = conv - .as_ref() - .map(|c| c.other_nickname.as_str()) - .unwrap_or("?"); - ui.label( - RichText::new(name) - .font(FontId::proportional(14.0)) - .color(t::TEXT_PRIMARY) - .strong(), - ); - }); - }); - - let r = ui.max_rect(); - ui.painter().hline( - r.x_range(), - r.top() + 40.0, - egui::Stroke::new(1.0, t::BORDER_SUBTLE), - ); - - let search_h = 32.0; - let input_h = 52.0; - messages::search_bar(ui, s, net, dm_id, search_h); - ui.painter().hline( - r.x_range(), - r.top() + 40.0 + search_h, - egui::Stroke::new(1.0, t::BORDER_SUBTLE), - ); - - messages::show( - ui, - s, - net, - identity, - dm_id, - ui.available_height() - input_h - search_h, - ); - messages::input_bar(ui, ctx, s, net, identity, dm_id); - }); -} diff --git a/src/ui/chat/friends/list.rs b/src/ui/chat/friends/list.rs deleted file mode 100644 index 918b030..0000000 --- a/src/ui/chat/friends/list.rs +++ /dev/null @@ -1,270 +0,0 @@ -use super::{muted_label, pending, section_header, status_color, status_label}; -use crate::state::{FriendState, UiState}; -use crate::theme as t; -use eframe::egui::{self, FontId, Frame, RichText, ScrollArea}; -use vnox_client::net::{NetCommand, NetHandle}; - -pub(super) fn render_content(ui: &mut egui::Ui, s: &mut UiState, net: &NetHandle) { - ScrollArea::vertical() - .auto_shrink([false, true]) - .show(ui, |ui| { - ui.add_space(4.0); - - match s.friends_tab.as_str() { - "online" => render_online(ui, s, net), - "pending" => render_pending(ui, s, net), - "blocked" => render_blocked(ui, s, net), - _ => render_all(ui, s, net), - } - }); -} - -fn render_online(ui: &mut egui::Ui, s: &mut UiState, net: &NetHandle) { - section_header(ui, "ONLINE"); - let online: Vec<&FriendState> = s - .friends - .iter() - .filter(|f| matches!(f.status.as_str(), "online" | "idle" | "dnd")) - .collect(); - if online.is_empty() { - muted_label(ui, "no friends online"); - } else { - for f in &online { - friend_row(ui, f, net); - } - } -} - -fn render_all(ui: &mut egui::Ui, s: &mut UiState, net: &NetHandle) { - section_header(ui, "ONLINE"); - let online: Vec<&FriendState> = s - .friends - .iter() - .filter(|f| matches!(f.status.as_str(), "online" | "idle" | "dnd")) - .collect(); - if online.is_empty() { - muted_label(ui, "no friends online"); - } else { - for f in &online { - friend_row(ui, f, net); - } - } - - ui.add_space(16.0); - - section_header(ui, "OFFLINE"); - let offline: Vec<&FriendState> = s - .friends - .iter() - .filter(|f| !matches!(f.status.as_str(), "online" | "idle" | "dnd")) - .collect(); - if offline.is_empty() { - if s.friends.is_empty() { - muted_label(ui, "no friends yet — add some!"); - } else { - muted_label(ui, "nobody is offline"); - } - } else { - for f in &offline { - friend_row(ui, f, net); - } - } -} - -fn render_pending(ui: &mut egui::Ui, s: &mut UiState, net: &NetHandle) { - section_header(ui, "PENDING — INCOMING"); - if s.pending_friend_requests.is_empty() { - muted_label(ui, "no pending requests"); - } else { - for req in &s.pending_friend_requests { - pending::pending_row(ui, req, net); - } - } -} - -fn render_blocked(ui: &mut egui::Ui, s: &mut UiState, net: &NetHandle) { - // Block-user input row - Frame::NONE - .inner_margin(egui::Margin::symmetric(12, 4)) - .show(ui, |ui| { - ui.horizontal(|ui| { - let mut buf = s.block_input.clone().unwrap_or_default(); - let resp = ui.add( - egui::TextEdit::singleline(&mut buf) - .font(egui::FontId::monospace(11.0)) - .hint_text("user id to block") - .desired_width(140.0) - .text_color(t::TEXT_PRIMARY), - ); - let mut trigger = false; - if resp.lost_focus() && ui.input(|i| i.key_pressed(egui::Key::Enter)) { - trigger = true; - } - if ui - .add( - egui::Button::new( - RichText::new("Block") - .font(FontId::proportional(10.0)) - .color(t::ERROR), - ) - .fill(t::BG_ELEVATED) - .stroke(egui::Stroke::new(1.0, t::BORDER_DEFAULT)) - .corner_radius(t::R_SM) - .min_size(egui::vec2(50.0, 18.0)), - ) - .clicked() - { - trigger = true; - } - s.block_input = if buf.is_empty() { None } else { Some(buf) }; - if trigger - && let Some(target) = s.block_input.clone().filter(|s| !s.trim().is_empty()) - { - let net = net.clone(); - let target = target.trim().to_string(); - tokio::spawn(async move { - net.send(NetCommand::BlockUser { user_id: target }).await; - }); - s.block_input = None; - } - }); - }); - ui.add_space(8.0); - - section_header(ui, "BLOCKED"); - if s.blocked_users.is_empty() { - muted_label(ui, "no blocked users — enter a user ID above to block"); - return; - } - - // Fetch on first open of this tab. - if s.block_list_fetched_at.is_none() { - s.block_list_fetched_at = Some(std::time::Instant::now()); - let net = net.clone(); - tokio::spawn(async move { - net.send(NetCommand::BlockList).await; - }); - } - - for uid in s.blocked_users.clone() { - Frame::NONE - .fill(t::BG_SURFACE) - .corner_radius(t::R_MD) - .inner_margin(egui::Margin::symmetric(12, 6)) - .show(ui, |ui| { - ui.horizontal(|ui| { - let init: String = uid.chars().next().unwrap_or('?').to_uppercase().to_string(); - t::avatar(ui, &init, t::TEXT_DISABLED, 28.0); - ui.add_space(8.0); - ui.vertical(|ui| { - ui.label( - RichText::new(&uid) - .font(FontId::monospace(11.0)) - .color(t::TEXT_PRIMARY), - ); - ui.label( - RichText::new("blocked") - .font(FontId::proportional(10.0)) - .color(t::TEXT_DISABLED), - ); - }); - ui.with_layout(egui::Layout::right_to_left(egui::Align::Center), |ui| { - if ui - .add( - egui::Button::new( - RichText::new("Unblock") - .font(FontId::proportional(11.0)) - .color(t::SUCCESS), - ) - .fill(egui::Color32::TRANSPARENT) - .frame(false), - ) - .on_hover_text("unblock this user") - .clicked() - { - let net = net.clone(); - let uidc = uid.clone(); - tokio::spawn(async move { - net.send(NetCommand::UnblockUser { user_id: uidc }).await; - }); - } - }); - }); - }); - } -} - -fn friend_row(ui: &mut egui::Ui, f: &FriendState, net: &NetHandle) { - Frame::NONE - .fill(t::BG_SURFACE) - .corner_radius(t::R_MD) - .inner_margin(egui::Margin::symmetric(12, 6)) - .show(ui, |ui| { - ui.horizontal(|ui| { - let init = f - .nickname - .chars() - .next() - .unwrap_or('?') - .to_uppercase() - .to_string(); - t::avatar(ui, &init, status_color(&f.status), 28.0); - - ui.add_space(8.0); - let (dot_r, _) = - ui.allocate_exact_size(egui::vec2(10.0, 10.0), egui::Sense::hover()); - ui.painter() - .circle_filled(dot_r.center(), 4.0, status_color(&f.status)); - - ui.add_space(6.0); - ui.vertical(|ui| { - ui.label( - RichText::new(&f.nickname) - .font(FontId::proportional(13.0)) - .color(t::TEXT_PRIMARY), - ); - ui.label( - RichText::new(status_label(&f.status)) - .font(FontId::proportional(10.0)) - .color(t::TEXT_DISABLED), - ); - }); - - ui.with_layout(egui::Layout::right_to_left(egui::Align::Center), |ui| { - if t::icon_btn_drawn( - ui, - crate::theme::icons::envelope, - "send message", - None, - 18.0, - ) - .clicked() - { - let net = net.clone(); - let uid = f.user_id.clone(); - tokio::spawn(async move { - net.send(NetCommand::DmStart { - target_user_id: uid, - }) - .await; - }); - } - if t::icon_btn_drawn( - ui, - crate::theme::icons::close, - "remove friend", - None, - 16.0, - ) - .clicked() - { - let net = net.clone(); - let uid = f.user_id.clone(); - tokio::spawn(async move { - net.send(NetCommand::FriendRemove { user_id: uid }).await; - }); - } - }); - }); - }); -} diff --git a/src/ui/chat/friends/mod.rs b/src/ui/chat/friends/mod.rs deleted file mode 100644 index b451700..0000000 --- a/src/ui/chat/friends/mod.rs +++ /dev/null @@ -1,209 +0,0 @@ -mod list; -mod pending; - -use eframe::egui::{self, FontId, Frame, RichText}; -use vnox_client::net::{NetCommand, NetHandle}; - -use crate::state::UiState; -use crate::theme as t; - -pub fn show(ui: &mut egui::Ui, s: &mut UiState, net: &NetHandle) { - ui.with_layout(egui::Layout::top_down_justified(egui::Align::LEFT), |ui| { - Frame::NONE - .fill(t::BG_SURFACE) - .inner_margin(egui::Margin::symmetric(16, 0)) - .show(ui, |ui| { - ui.set_height(44.0); - ui.horizontal_centered(|ui| { - ui.label( - RichText::new("Friends") - .font(FontId::proportional(16.0)) - .color(t::TEXT_PRIMARY) - .strong(), - ); - ui.with_layout(egui::Layout::right_to_left(egui::Align::Center), |ui| { - let add_btn = egui::Button::new( - RichText::new("Add Friend") - .font(FontId::proportional(11.0)) - .color(t::ACCENT_SOFT), - ) - .fill(t::ACCENT) - .stroke(egui::Stroke::NONE) - .corner_radius(t::R_SM) - .min_size(egui::vec2(80.0, 26.0)); - if ui.add(add_btn).clicked() { - s.add_friend_open = true; - s.add_friend_input.clear(); - } - }); - }); - }); - - let sep = ui.max_rect(); - ui.painter().hline( - sep.x_range(), - sep.top() + 44.0, - egui::Stroke::new(1.0, t::BORDER_SUBTLE), - ); - - ui.add_space(8.0); - - // Pending count badge for the Pending tab - let pending_count = s.pending_friend_requests.len(); - - ui.horizontal(|ui| { - ui.add_space(16.0); - ui.spacing_mut().item_spacing.x = 4.0; - for (tab_key, label) in [ - ("online", "Online"), - ("all", "All"), - ("pending", "Pending"), - ("blocked", "Blocked"), - ] { - let selected = s.friends_tab == tab_key; - let display = if tab_key == "pending" && pending_count > 0 { - format!("{label} ({pending_count})") - } else { - label.to_string() - }; - let btn = egui::Button::new( - RichText::new(display) - .font(FontId::proportional(11.0)) - .color(if selected { - t::TEXT_PRIMARY - } else { - t::TEXT_MUTED - }), - ) - .fill(if selected { - t::BG_ELEVATED - } else { - egui::Color32::TRANSPARENT - }) - .stroke(egui::Stroke::NONE) - .corner_radius(t::R_SM); - if ui.add(btn).clicked() { - s.friends_tab = tab_key.to_string(); - } - } - }); - - ui.add_space(8.0); - - list::render_content(ui, s, net); - }); - - if s.add_friend_open { - show_add_friend_popup(ui.ctx(), s, net); - } -} - -fn show_add_friend_popup(ctx: &egui::Context, s: &mut UiState, net: &NetHandle) { - egui::Window::new("Add Friend") - .collapsible(false) - .resizable(false) - .anchor(egui::Align2::CENTER_CENTER, [0.0, 0.0]) - .frame( - Frame::window(&ctx.style()) - .fill(t::BG_ELEVATED) - .stroke(egui::Stroke::new(1.0, t::BORDER_DEFAULT)) - .corner_radius(t::R_LG), - ) - .show(ctx, |ui| { - ui.set_width(320.0); - ui.label( - RichText::new("Enter a user ID to send a friend request.") - .font(FontId::proportional(12.0)) - .color(t::TEXT_MUTED), - ); - ui.add_space(8.0); - let resp = ui.add( - egui::TextEdit::singleline(&mut s.add_friend_input) - .font(FontId::monospace(13.0)) - .text_color(t::TEXT_PRIMARY) - .hint_text(RichText::new("user id (pubkey hex)").color(t::TEXT_DISABLED)) - .desired_width(280.0), - ); - ui.add_space(12.0); - ui.horizontal(|ui| { - if ui - .add( - egui::Button::new( - RichText::new("Cancel") - .font(FontId::proportional(12.0)) - .color(t::TEXT_MUTED), - ) - .fill(egui::Color32::TRANSPARENT) - .frame(false), - ) - .clicked() - { - s.add_friend_open = false; - } - ui.with_layout(egui::Layout::right_to_left(egui::Align::Center), |ui| { - let send_btn = egui::Button::new( - RichText::new("Send Request") - .font(FontId::proportional(12.0)) - .color(egui::Color32::WHITE), - ) - .fill(t::ACCENT) - .stroke(egui::Stroke::NONE) - .corner_radius(t::R_SM) - .min_size(egui::vec2(110.0, 28.0)); - if ui.add(send_btn).clicked() && !s.add_friend_input.trim().is_empty() { - let net = net.clone(); - let target = s.add_friend_input.trim().to_string(); - tokio::spawn(async move { - net.send(NetCommand::FriendRequest { - target_user_id: target, - }) - .await; - }); - s.add_friend_open = false; - } - }); - }); - if resp.lost_focus() && ui.input(|i| i.key_pressed(egui::Key::Escape)) { - s.add_friend_open = false; - } - }); -} - -pub(super) fn section_header(ui: &mut egui::Ui, label: &str) { - ui.add_space(4.0); - ui.label( - RichText::new(label) - .font(FontId::proportional(10.0)) - .color(t::TEXT_MUTED) - .strong(), - ); - ui.add_space(2.0); -} - -pub(super) fn muted_label(ui: &mut egui::Ui, text: &str) { - ui.add_space(4.0); - ui.label( - RichText::new(text) - .font(FontId::proportional(12.0)) - .color(t::TEXT_DISABLED), - ); -} - -pub(super) fn status_color(status: &str) -> egui::Color32 { - match status { - "online" => t::SUCCESS, - "idle" => t::WARNING, - "dnd" => t::ERROR, - _ => t::TEXT_DISABLED, - } -} - -pub(super) fn status_label(status: &str) -> &str { - match status { - "online" => "online", - "idle" => "idle", - "dnd" => "do not disturb", - "offline" => "offline", - _ => status, - } -} diff --git a/src/ui/chat/friends/pending.rs b/src/ui/chat/friends/pending.rs deleted file mode 100644 index 1ede2cd..0000000 --- a/src/ui/chat/friends/pending.rs +++ /dev/null @@ -1,62 +0,0 @@ -use eframe::egui::{self, FontId, Frame, RichText}; -use vnox_client::net::{NetCommand, NetHandle}; - -use crate::state::FriendState; -use crate::theme as t; - -pub(super) fn pending_row(ui: &mut egui::Ui, f: &FriendState, net: &NetHandle) { - Frame::NONE - .fill(t::BG_SURFACE) - .corner_radius(t::R_MD) - .inner_margin(egui::Margin::symmetric(12, 6)) - .show(ui, |ui| { - ui.horizontal(|ui| { - let init = f - .nickname - .chars() - .next() - .unwrap_or('?') - .to_uppercase() - .to_string(); - t::avatar(ui, &init, t::WARNING, 28.0); - ui.add_space(8.0); - ui.label( - RichText::new(&f.nickname) - .font(FontId::proportional(13.0)) - .color(t::TEXT_PRIMARY), - ); - ui.with_layout(egui::Layout::right_to_left(egui::Align::Center), |ui| { - if t::icon_btn_drawn( - ui, - crate::theme::icons::check, - "accept", - Some(t::SUCCESS), - 18.0, - ) - .clicked() - { - let net = net.clone(); - let uid = f.user_id.clone(); - tokio::spawn(async move { - net.send(NetCommand::FriendAccept { user_id: uid }).await; - }); - } - if t::icon_btn_drawn( - ui, - crate::theme::icons::close, - "decline", - Some(t::ERROR), - 18.0, - ) - .clicked() - { - let net = net.clone(); - let uid = f.user_id.clone(); - tokio::spawn(async move { - net.send(NetCommand::FriendDecline { user_id: uid }).await; - }); - } - }); - }); - }); -} diff --git a/src/ui/chat/mod.rs b/src/ui/chat/mod.rs deleted file mode 100644 index dac2e4b..0000000 --- a/src/ui/chat/mod.rs +++ /dev/null @@ -1,66 +0,0 @@ -use eframe::egui::{self, Context, FontId, RichText}; - -use crate::state::UiState; -use crate::theme as t; -use vnox_client::{identity::Identity, net::NetHandle}; - -mod dm; -mod friends; -mod text; -mod voice; - -pub use friends::show as friends_panel; - -pub fn show( - ui: &mut egui::Ui, - ctx: &Context, - s: &mut UiState, - identity: &Identity, - net: &NetHandle, -) { - // If a DM conversation is active, show the DM chat view - if let Some(dm_id) = s.active_dm_id.clone() { - // Clear any active channel when showing DM - s.active_channel = None; - dm::dm_chat(ui, ctx, s, identity, net, &dm_id); - return; - } - - // Home screen — show Friends panel if no guild and no DM - if s.active_guild_id.is_none() { - friends::show(ui, s, net); - return; - } - - let ch_id = match s.active_channel.clone() { - Some(id) => id, - None => { - empty_state(ui, "pick a channel"); - return; - } - }; - - let ch = match s.channels.iter().find(|c| c.id == ch_id) { - Some(c) => c.clone(), - None => { - empty_state(ui, "channel not found"); - return; - } - }; - - if ch.kind == "voice" { - voice::voice_panel(ui, s, &ch); - } else { - text::text_chat(ui, ctx, s, net, identity, &ch_id, &ch.name); - } -} - -fn empty_state(ui: &mut egui::Ui, text: &str) { - ui.vertical_centered(|ui| { - ui.label( - RichText::new(text) - .font(FontId::monospace(12.0)) - .color(t::TEXT_DEAD), - ); - }); -} diff --git a/src/ui/chat/text/input.rs b/src/ui/chat/text/input.rs deleted file mode 100644 index bbad55c..0000000 --- a/src/ui/chat/text/input.rs +++ /dev/null @@ -1,186 +0,0 @@ -use crate::state::{ChatMessage, UiState}; -use crate::theme as t; -use eframe::egui::{self, Context, FontId, Frame, RichText}; -use std::collections::HashMap; -use std::time::Instant; -use vnox_client::{ - identity::Identity, - net::{NetCommand, NetHandle}, -}; - -pub(crate) fn chat_input_bar( - ui: &mut egui::Ui, - ctx: &Context, - s: &mut UiState, - net: &NetHandle, - ch_id: &str, - identity: &Identity, -) { - let r = ui.available_rect_before_wrap(); - ui.painter().hline( - r.x_range(), - r.top(), - egui::Stroke::new(1.0, t::BORDER_SUBTLE), - ); - - Frame::NONE - .fill(t::BG_SURFACE) - .inner_margin(egui::Margin::symmetric(12, 8)) - .show(ui, |ui| { - // Reply / edit preview bar - if let Some((ref _reply_id, ref reply_sender)) = s.replying_to.clone() { - ui.horizontal(|ui| { - ui.label( - RichText::new("replying to") - .font(FontId::proportional(11.0)) - .color(t::ACCENT), - ); - let sender_nick = s.nick_for(reply_sender).to_string(); - ui.label( - RichText::new(sender_nick) - .font(FontId::proportional(11.0)) - .color(t::TEXT_SECONDARY), - ); - if t::icon_btn_drawn(ui, crate::theme::icons::close, "cancel reply", None, 14.0) - .clicked() - { - s.replying_to = None; - } - }); - } - if s.editing_message_id.is_some() { - ui.horizontal(|ui| { - ui.label( - RichText::new("editing") - .font(FontId::proportional(11.0)) - .color(t::WARNING), - ); - if t::icon_btn_drawn(ui, crate::theme::icons::close, "cancel edit", None, 14.0) - .clicked() - { - s.chat_input.clear(); - s.editing_message_id = None; - } - }); - } - ui.horizontal(|ui| { - let hint_text = if s.editing_message_id.is_some() { - "edit your message...".to_string() - } else if s.replying_to.is_some() { - "write your reply...".to_string() - } else { - format!("message in #{ch_id}") - }; - let input = egui::TextEdit::singleline(&mut s.chat_input) - .font(FontId::proportional(13.0)) - .text_color(t::TEXT_PRIMARY) - .hint_text(RichText::new(hint_text).color(t::TEXT_DISABLED)) - .frame(false) - .desired_width(ui.available_width() - 44.0); - - let enter = Frame::NONE - .fill(t::BG_INTERACTIVE) - .corner_radius(t::R_MD) - .stroke(egui::Stroke::new(1.0, t::BORDER_DEFAULT)) - .inner_margin(egui::Margin::symmetric(12, 6)) - .show(ui, |ui| ui.add(input)) - .inner; - - if !s.chat_input.is_empty() && s.last_typing_send.elapsed().as_secs() >= 3 { - s.last_typing_send = Instant::now(); - let net = net.clone(); - let c = ch_id.to_string(); - tokio::spawn(async move { - net.send(NetCommand::TypingStart { channel_id: c }).await; - }); - } - - let submit = (enter.lost_focus() && ui.input(|i| i.key_pressed(egui::Key::Enter))) - || ui - .add( - egui::Button::new(RichText::new("→").color(t::ACCENT).size(14.0)) - .fill(t::ACCENT_10) - .stroke(egui::Stroke::new(1.0, t::BORDER_ACCENT)) - .corner_radius(t::R_MD) - .min_size(egui::vec2(32.0, 32.0)), - ) - .clicked(); - - if submit && !s.chat_input.trim().is_empty() { - if let Some(edit_id) = s.editing_message_id.clone() { - let content = s.chat_input.trim().to_string(); - s.chat_input.clear(); - s.editing_message_id = None; - let net = net.clone(); - let cid = ch_id.to_string(); - tokio::spawn(async move { - net.send(NetCommand::MessageEdit { - channel_id: cid, - message_id: edit_id, - content, - }) - .await; - }); - } else { - let reply_to = s.replying_to.clone().map(|(id, _)| id); - send_chat(s, net, ch_id, &identity.pubkey_hex, reply_to); - s.replying_to = None; - } - ctx.request_repaint(); - } - }); - }); -} - -fn send_chat( - s: &mut UiState, - net: &NetHandle, - ch_id: &str, - sender_id: &str, - reply_to: Option, -) { - let content = s.chat_input.trim().to_string(); - if content.is_empty() { - return; - } - s.chat_input.clear(); - - s.messages - .entry(ch_id.to_string()) - .or_default() - .push(ChatMessage { - message_id: uuid::Uuid::new_v4().to_string(), - sender_id: sender_id.to_string(), - content: content.clone(), - timestamp: std::time::SystemTime::now() - .duration_since(std::time::UNIX_EPOCH) - .unwrap_or_default() - .as_millis() as i64, - edited: false, - reactions: HashMap::new(), - reply_to: reply_to.clone(), - }); - s.scroll_bottom = true; - - let need_join = s.server_channel.as_deref() != Some(ch_id); - if need_join { - s.server_channel = Some(ch_id.to_string()); - } - - let net = net.clone(); - let ch = ch_id.to_string(); - tokio::spawn(async move { - if need_join { - net.send(NetCommand::JoinChannel { - channel_id: ch.clone(), - }) - .await; - } - net.send(NetCommand::SendChat { - channel_id: ch, - content, - reply_to, - }) - .await; - }); -} diff --git a/src/ui/chat/text/messages/list.rs b/src/ui/chat/text/messages/list.rs deleted file mode 100644 index 7c68bb8..0000000 --- a/src/ui/chat/text/messages/list.rs +++ /dev/null @@ -1,75 +0,0 @@ -use crate::state::{TYPING_TIMEOUT, UiState}; -use crate::theme as t; -use eframe::egui::{self, FontId, RichText, ScrollArea}; -use std::time::Instant; -use vnox_client::{identity::Identity, net::NetHandle}; - -use super::row::message_row; - -pub(crate) fn show( - ui: &mut egui::Ui, - s: &mut UiState, - net: &NetHandle, - identity: &Identity, - ch_id: &str, - max_h: f32, -) { - let msgs = s.messages.get(ch_id).cloned().unwrap_or_default(); - ScrollArea::vertical() - .max_height(max_h) - .auto_shrink([false; 2]) - .stick_to_bottom(s.scroll_bottom) - .show(ui, |ui| { - ui.add_space(12.0); - if msgs.is_empty() { - ui.label( - RichText::new("no messages yet") - .font(FontId::proportional(12.0)) - .color(t::TEXT_DISABLED), - ); - } else { - ui.label( - RichText::new("today") - .font(FontId::proportional(11.0)) - .color(t::TEXT_DISABLED), - ); - ui.add_space(8.0); - let mut prev: Option = None; - for msg in &msgs { - let group_start = prev.as_deref() != Some(&msg.sender_id); - message_row(ui, s, net, identity, msg, ch_id, group_start); - prev = Some(msg.sender_id.clone()); - } - } - typing_indicator(ui, s, ch_id); - ui.add_space(8.0); - }); - s.scroll_bottom = false; -} - -fn typing_indicator(ui: &mut egui::Ui, s: &mut UiState, ch_id: &str) { - let now = Instant::now(); - if let Some(typers) = s.typing_users.get_mut(ch_id) { - typers.retain(|(_, t)| now.duration_since(*t) < TYPING_TIMEOUT); - if typers.is_empty() { - s.typing_users.remove(ch_id); - return; - } - let names: Vec<&str> = typers.iter().map(|(n, _)| n.as_str()).collect(); - let label = if names.len() == 1 { - format!("{} is typing...", names[0]) - } else { - format!( - "{} and {} are typing...", - names[..names.len() - 1].join(", "), - names[names.len() - 1] - ) - }; - ui.add_space(4.0); - ui.label( - RichText::new(label) - .font(FontId::proportional(11.0)) - .color(t::TEXT_DISABLED), - ); - } -} diff --git a/src/ui/chat/text/messages/mod.rs b/src/ui/chat/text/messages/mod.rs deleted file mode 100644 index 7f68ad6..0000000 --- a/src/ui/chat/text/messages/mod.rs +++ /dev/null @@ -1,130 +0,0 @@ -pub mod list; -pub mod row; - -pub(crate) use list::show; -pub use row::message_row; - -use crate::state::{ChatMessage, UiState}; -use crate::theme as t; -use crate::theme::icons; -use eframe::egui::{self, RichText}; -use vnox_client::{ - identity::Identity, - net::{NetCommand, NetHandle}, -}; - -fn own_actions( - ui: &mut egui::Ui, - s: &mut UiState, - net: &NetHandle, - msg: &ChatMessage, - ch_id: &str, -) { - if t::icon_btn_drawn(ui, icons::pencil, "edit", None, 16.0).clicked() { - s.chat_input = msg.content.clone(); - s.editing_message_id = Some(msg.message_id.clone()); - } - if t::icon_btn_drawn(ui, icons::trash, "delete", None, 16.0).clicked() { - let net = net.clone(); - let mid = msg.message_id.clone(); - let cid = ch_id.to_string(); - tokio::spawn(async move { - net.send(NetCommand::MessageDelete { - channel_id: cid, - message_id: mid, - }) - .await; - }); - } -} - -fn reaction_buttons( - ui: &mut egui::Ui, - net: &NetHandle, - msg: &ChatMessage, - ch_id: &str, - identity: &Identity, -) { - ui.horizontal(|ui| { - ui.spacing_mut().item_spacing.x = 4.0; - for (emoji, users) in &msg.reactions { - let count = users.len(); - let has_reacted = users.iter().any(|u| u == &identity.pubkey_hex); - let bg = if has_reacted { - t::ACCENT_20 - } else { - t::BG_ELEVATED - }; - let border = if has_reacted { - t::BORDER_ACCENT - } else { - t::BORDER_SUBTLE - }; - let label = format!("{} {}", emoji, count); - if ui - .add( - egui::Button::new(RichText::new(&label).size(12.0).color(t::TEXT_SECONDARY)) - .fill(bg) - .stroke(egui::Stroke::new(1.0, border)) - .corner_radius(4.0) - .min_size(egui::Vec2::ZERO), - ) - .clicked() - { - let net = net.clone(); - let mid = msg.message_id.clone(); - let cid = ch_id.to_string(); - let e = emoji.clone(); - if has_reacted { - tokio::spawn(async move { - net.send(NetCommand::ReactionRemove { - channel_id: cid, - message_id: mid, - emoji: e, - }) - .await; - }); - } else { - tokio::spawn(async move { - net.send(NetCommand::ReactionAdd { - channel_id: cid, - message_id: mid, - emoji: e, - }) - .await; - }); - } - } - } - }); -} - -fn read_receipt_indicator(ui: &mut egui::Ui, s: &UiState) { - if let Some(ch) = &s.active_channel - && s.read_receipts.contains_key(ch) - { - ui.add_space(6.0); - let (rr, _) = ui.allocate_exact_size(egui::vec2(12.0, 12.0), egui::Sense::hover()); - icons::check(ui.painter(), rr, t::TEXT_DISABLED); - } -} - -fn fmt_ts(ts: i64) -> String { - let s = (ts / 1000).abs() % 86400; - format!("{:02}:{:02}", s / 3600, (s % 3600) / 60) -} - -fn nick_color(s: &UiState, sender_id: &str) -> egui::Color32 { - if let Some(color) = s.user_colors.get(sender_id) { - return *color; - } - let nick = s.nick_for(sender_id); - let c = [ - t::ACCENT, - t::SUCCESS, - t::WARNING, - t::INFO, - egui::Color32::from_rgb(200, 100, 180), - ]; - c[nick.bytes().fold(0usize, |a, b| a.wrapping_add(b as usize)) % c.len()] -} diff --git a/src/ui/chat/text/messages/row.rs b/src/ui/chat/text/messages/row.rs deleted file mode 100644 index c49b196..0000000 --- a/src/ui/chat/text/messages/row.rs +++ /dev/null @@ -1,220 +0,0 @@ -use crate::state::{ChatMessage, UiState}; -use crate::theme as t; -use eframe::egui::{self, FontId, Frame, RichText}; -use vnox_client::{ - identity::Identity, - net::{NetCommand, NetHandle}, -}; - -use super::{fmt_ts, nick_color, own_actions, reaction_buttons, read_receipt_indicator}; - -pub fn message_row( - ui: &mut egui::Ui, - s: &mut UiState, - net: &NetHandle, - identity: &Identity, - msg: &ChatMessage, - ch_id: &str, - group_start: bool, -) { - let nick = s.nick_for(&msg.sender_id).to_string(); - let is_own = msg.sender_id == identity.pubkey_hex; - let msg_id = msg.message_id.clone(); - let sender_id = msg.sender_id.clone(); - let content_clone = msg.content.clone(); - - let row_response = Frame::NONE - .inner_margin(egui::Margin { - left: 16, - right: 16, - top: if group_start { 6 } else { 1 }, - bottom: 0, - }) - .show(ui, |ui| { - ui.horizontal(|ui| { - ui.spacing_mut().item_spacing.x = 10.0; - if group_start { - let init = nick - .chars() - .next() - .unwrap_or('?') - .to_uppercase() - .to_string(); - t::avatar(ui, &init, nick_color(s, &msg.sender_id), 32.0); - } else { - ui.add_space(32.0); - } - ui.vertical(|ui| { - ui.spacing_mut().item_spacing.y = 1.0; - if group_start { - ui.horizontal(|ui| { - ui.label( - RichText::new(&nick) - .font(FontId::proportional(13.0)) - .color(t::TEXT_PRIMARY) - .strong(), - ); - ui.label( - RichText::new(fmt_ts(msg.timestamp)) - .font(FontId::proportional(11.0)) - .color(t::TEXT_DISABLED), - ); - if msg.edited { - ui.label( - RichText::new("(edited)") - .font(FontId::proportional(10.0)) - .color(t::TEXT_DISABLED), - ); - } - if is_own { - own_actions(ui, s, net, msg, ch_id); - } - }); - } - // Reply indicator - if let Some(reply_id) = &msg.reply_to { - let reply_snippet = s - .messages - .get(ch_id) - .and_then(|msgs| msgs.iter().find(|m| &m.message_id == reply_id)) - .map(|m| { - let reply_nick = s.nick_for(&m.sender_id).to_string(); - let snippet: String = m.content.chars().take(60).collect(); - format!("→ {reply_nick}: {snippet}") - }) - .unwrap_or_else(|| "→ (deleted message)".to_string()); - ui.label( - RichText::new(reply_snippet) - .font(FontId::proportional(11.0)) - .color(t::TEXT_MUTED) - .italics(), - ); - } - ui.horizontal(|ui| { - ui.label( - RichText::new(&msg.content) - .font(FontId::proportional(13.0)) - .color(t::TEXT_SECONDARY), - ); - read_receipt_indicator(ui, s); - }); - if !msg.reactions.is_empty() { - reaction_buttons(ui, net, msg, ch_id, identity); - } - }); - }); - }) - .response; - - // Right-click context menu (egui 0.33 API: Response::context_menu). - row_response.context_menu(|ui| { - s.context_menu_message_id = Some(msg_id.clone()); - let quick_reactions = ["👍", "❤️", "😂", "🎉", "👀", "🤔"]; - ui.horizontal(|ui| { - for emoji in quick_reactions { - let has_reacted = msg - .reactions - .get(emoji) - .is_some_and(|users| users.contains(&identity.pubkey_hex)); - let btn_text = if has_reacted { - format!("{emoji} +") - } else { - emoji.to_string() - }; - if ui - .button(RichText::new(btn_text).font(FontId::proportional(14.0))) - .clicked() - { - let net = net.clone(); - let mid = msg_id.clone(); - let cid = ch_id.to_string(); - let e = emoji.to_string(); - if has_reacted { - tokio::spawn(async move { - net.send(NetCommand::ReactionRemove { - channel_id: cid, - message_id: mid, - emoji: e, - }) - .await; - }); - } else { - tokio::spawn(async move { - net.send(NetCommand::ReactionAdd { - channel_id: cid, - message_id: mid, - emoji: e, - }) - .await; - }); - } - ui.close(); - } - } - }); - ui.separator(); - if ui - .add( - egui::Button::new(RichText::new("Reply").font(FontId::proportional(12.0))) - .fill(egui::Color32::TRANSPARENT) - .frame(false), - ) - .clicked() - { - s.replying_to = Some((msg_id.clone(), sender_id.clone())); - s.editing_message_id = None; - ui.close(); - } - if ui - .add( - egui::Button::new(RichText::new("Copy text").font(FontId::proportional(12.0))) - .fill(egui::Color32::TRANSPARENT) - .frame(false), - ) - .clicked() - { - ui.ctx().copy_text(content_clone.clone()); - ui.close(); - } - if is_own { - ui.separator(); - if ui - .add( - egui::Button::new(RichText::new("Edit").font(FontId::proportional(12.0))) - .fill(egui::Color32::TRANSPARENT) - .frame(false), - ) - .clicked() - { - s.chat_input = content_clone.clone(); - s.editing_message_id = Some(msg_id.clone()); - s.replying_to = None; - ui.close(); - } - if ui - .add( - egui::Button::new( - RichText::new("Delete") - .font(FontId::proportional(12.0)) - .color(t::ERROR), - ) - .fill(egui::Color32::TRANSPARENT) - .frame(false), - ) - .clicked() - { - let net = net.clone(); - let mid = msg_id.clone(); - let cid = ch_id.to_string(); - tokio::spawn(async move { - net.send(NetCommand::MessageDelete { - channel_id: cid, - message_id: mid, - }) - .await; - }); - ui.close(); - } - } - }); -} diff --git a/src/ui/chat/text/mod.rs b/src/ui/chat/text/mod.rs deleted file mode 100644 index ab6886f..0000000 --- a/src/ui/chat/text/mod.rs +++ /dev/null @@ -1,77 +0,0 @@ -mod input; -mod messages; - -use eframe::egui::{self, Context, FontId, Frame, RichText}; -use vnox_client::{identity::Identity, net::NetHandle}; - -use crate::state::UiState; -use crate::theme as t; - -pub(super) use messages::message_row; - -pub(crate) fn text_chat( - ui: &mut egui::Ui, - ctx: &Context, - s: &mut UiState, - net: &NetHandle, - identity: &Identity, - ch_id: &str, - ch_name: &str, -) { - ui.with_layout(egui::Layout::top_down_justified(egui::Align::LEFT), |ui| { - Frame::NONE - .fill(t::BG_SURFACE) - .inner_margin(egui::Margin::symmetric(16, 0)) - .show(ui, |ui| { - ui.set_height(40.0); - ui.horizontal_centered(|ui| { - ui.label(RichText::new("#").color(t::TEXT_MUTED).size(16.0)); - ui.add_space(4.0); - ui.label( - RichText::new(ch_name) - .font(FontId::proportional(14.0)) - .color(t::TEXT_PRIMARY) - .strong(), - ); - }); - }); - let r = ui.max_rect(); - ui.painter().hline( - r.x_range(), - r.top() + 40.0, - egui::Stroke::new(1.0, t::BORDER_SUBTLE), - ); - - if let Some(elapsed) = s.voice_elapsed() { - voice_banner(ui, s, &elapsed); - } - - let input_h = 52.0; - messages::show(ui, s, net, identity, ch_id, ui.available_height() - input_h); - input::chat_input_bar(ui, ctx, s, net, ch_id, identity); - }); -} - -fn voice_banner(ui: &mut egui::Ui, s: &UiState, elapsed: &str) { - Frame::NONE - .fill(t::BG_ELEVATED) - .inner_margin(egui::Margin::symmetric(16, 5)) - .show(ui, |ui| { - ui.horizontal(|ui| { - let (dot, _) = ui.allocate_exact_size(egui::vec2(7.0, 7.0), egui::Sense::hover()); - ui.painter().circle_filled(dot.center(), 3.5, t::SUCCESS); - ui.label(RichText::new("voice").color(t::SUCCESS).size(12.0)); - ui.label(RichText::new("·").color(t::TEXT_DISABLED).size(12.0)); - if let Some(ch) = s.active_voice_channel() { - ui.label(RichText::new(&ch.name).color(t::TEXT_SECONDARY).size(12.0)); - } - ui.with_layout(egui::Layout::right_to_left(egui::Align::Center), |ui| { - ui.label( - RichText::new(elapsed) - .font(FontId::monospace(11.0)) - .color(t::TEXT_DISABLED), - ); - }); - }); - }); -} diff --git a/src/ui/chat/voice/mod.rs b/src/ui/chat/voice/mod.rs deleted file mode 100644 index 8b792d4..0000000 --- a/src/ui/chat/voice/mod.rs +++ /dev/null @@ -1,147 +0,0 @@ -mod panel; - -use eframe::egui::{self, FontId, Frame, RichText}; - -use crate::state::{Channel, UiState, VoiceUiState}; -use crate::theme as t; - -pub(crate) fn voice_panel(ui: &mut egui::Ui, s: &mut UiState, ch: &Channel) { - ui.with_layout(egui::Layout::top_down_justified(egui::Align::LEFT), |ui| { - Frame::NONE - .fill(t::BG_SURFACE) - .inner_margin(egui::Margin::symmetric(16, 0)) - .show(ui, |ui| { - ui.set_height(40.0); - ui.horizontal_centered(|ui| { - ui.label(RichText::new("~").color(t::TEXT_MUTED).size(15.0)); - ui.add_space(4.0); - ui.label( - RichText::new(&ch.name) - .font(FontId::proportional(14.0)) - .color(t::TEXT_PRIMARY) - .strong(), - ); - if let Some(elapsed) = s.voice_elapsed() { - ui.with_layout(egui::Layout::right_to_left(egui::Align::Center), |ui| { - ui.label( - RichText::new(elapsed) - .font(FontId::monospace(11.0)) - .color(t::TEXT_DISABLED), - ); - }); - } - }); - }); - - let r = ui.max_rect(); - ui.painter().hline( - r.x_range(), - r.top() + 40.0, - egui::Stroke::new(1.0, t::BORDER_SUBTLE), - ); - - ui.add_space(16.0); - Frame::NONE - .inner_margin(egui::Margin::symmetric(20, 0)) - .show(ui, |ui| { - // Voice activity banner — shows when local or remote user is speaking - if s.local_speaking || s.remote_speaking { - voice_activity_banner(ui, s); - ui.add_space(8.0); - } - if ch.members.is_empty() { - ui.label( - RichText::new("no members yet") - .font(FontId::proportional(12.0)) - .color(t::TEXT_DISABLED), - ); - } - // Identify which member is currently speaking by sender_id. - let speaking_uid = if !s.last_remote_speaker_id.is_empty() { - Some(s.last_remote_speaker_id.clone()) - } else { - None - }; - for nick in &ch.members { - let user_id = s - .user_names - .iter() - .find(|(_, n)| *n == nick) - .map(|(id, _)| id.clone()) - .unwrap_or_else(|| nick.clone()); - let member_speaking = speaking_uid.as_deref() == Some(&user_id); - panel::member_card( - ui, - nick, - &user_id, - &mut s.per_user_volumes, - member_speaking, - ); - ui.add_space(6.0); - } - }); - - if let VoiceUiState::Error(ref err) = s.voice { - ui.add_space(16.0); - Frame::NONE - .fill(egui::Color32::from_rgba_premultiplied(217, 96, 74, 20)) - .corner_radius(t::R_MD) - .stroke(egui::Stroke::new( - 1.0, - egui::Color32::from_rgba_premultiplied(217, 96, 74, 60), - )) - .inner_margin(egui::Margin::same(12)) - .show(ui, |ui| { - ui.label( - RichText::new(format!("microphone error: {err}")) - .font(FontId::proportional(12.0)) - .color(t::ERROR), - ); - }); - } - - ui.with_layout(egui::Layout::bottom_up(egui::Align::Center), |ui| { - ui.add_space(16.0); - panel::voice_controls(ui, s); - }); - }); -} - -fn voice_activity_banner(ui: &mut egui::Ui, s: &UiState) { - let remote_nick: Option = if !s.last_remote_speaker_id.is_empty() { - Some(s.nick_for(&s.last_remote_speaker_id).to_string()) - } else { - None - }; - let (label, color) = match (s.local_speaking, s.remote_speaking, remote_nick.as_deref()) { - (true, true, Some(nick)) => (format!("you and {nick} are talking"), t::SUCCESS), - (true, true, None) => ("you and others are talking".to_string(), t::SUCCESS), - (true, false, _) => ("you are talking".to_string(), t::SUCCESS), - (false, true, Some(nick)) => (format!("{nick} is talking"), t::INFO), - (false, true, None) => ("someone is talking".to_string(), t::INFO), - _ => return, - }; - Frame::NONE - .fill(egui::Color32::from_rgba_premultiplied( - color.r(), - color.g(), - color.b(), - 20, - )) - .corner_radius(t::R_MD) - .stroke(egui::Stroke::new(1.0, color)) - .inner_margin(egui::Margin::symmetric(12, 6)) - .show(ui, |ui| { - ui.horizontal(|ui| { - // Pulsating dot - let (r, _) = ui.allocate_exact_size(egui::vec2(10.0, 10.0), egui::Sense::hover()); - ui.painter().circle_filled(r.center(), 5.0, color); - ui.label( - RichText::new(label) - .font(FontId::proportional(12.0)) - .color(color) - .strong(), - ); - }); - }); -} diff --git a/src/ui/chat/voice/panel.rs b/src/ui/chat/voice/panel.rs deleted file mode 100644 index ee0a3d0..0000000 --- a/src/ui/chat/voice/panel.rs +++ /dev/null @@ -1,199 +0,0 @@ -use eframe::egui::{self, FontId, Frame, Rect, RichText}; - -use crate::state::UiState; -use crate::theme as t; -use crate::theme::icons; - -pub(super) fn member_card( - ui: &mut egui::Ui, - nick: &str, - user_id: &str, - volumes: &mut std::collections::HashMap, - speaking: bool, -) { - let vol = volumes.entry(user_id.to_string()).or_insert(80.0); - let avatar_color = if speaking { - t::SUCCESS - } else { - nick_color(nick) - }; - Frame::NONE - .fill(if speaking { - egui::Color32::from_rgba_premultiplied( - t::SUCCESS.r(), - t::SUCCESS.g(), - t::SUCCESS.b(), - 15, - ) - } else { - t::BG_ELEVATED - }) - .corner_radius(t::R_MD) - .stroke(if speaking { - egui::Stroke::new(1.0, t::SUCCESS) - } else { - egui::Stroke::new(1.0, t::BORDER_DEFAULT) - }) - .inner_margin(egui::Margin::symmetric(12, 8)) - .show(ui, |ui| { - ui.horizontal(|ui| { - let init = nick - .chars() - .next() - .unwrap_or('?') - .to_uppercase() - .to_string(); - t::avatar(ui, &init, avatar_color, 28.0); - ui.add_space(8.0); - ui.vertical(|ui| { - ui.horizontal(|ui| { - ui.label( - RichText::new(nick) - .font(FontId::proportional(13.0)) - .color(if speaking { - t::SUCCESS - } else { - t::TEXT_PRIMARY - }) - .strong(), - ); - if speaking { - let (sr, _) = ui - .allocate_exact_size(egui::vec2(14.0, 14.0), egui::Sense::hover()); - icons::speaker(ui.painter(), sr, t::SUCCESS); - } - }); - ui.horizontal(|ui| { - ui.label( - RichText::new("vol") - .font(FontId::proportional(9.0)) - .color(t::TEXT_DISABLED), - ); - ui.add( - egui::Slider::new(vol, 0.0..=100.0) - .show_value(false) - .smallest_positive(0.0), - ); - }); - }); - }); - }); -} - -pub(super) fn voice_controls(ui: &mut egui::Ui, s: &mut UiState) { - ui.horizontal(|ui| { - ui.spacing_mut().item_spacing.x = 8.0; - - // Microphone button — drawn icon. - let mic_color = if s.mic_enabled { - t::TEXT_SECONDARY - } else { - t::ERROR - }; - let mic_fill = if s.mic_enabled { - t::BG_ELEVATED - } else { - egui::Color32::from_rgba_premultiplied(217, 96, 74, 20) - }; - let mic_stroke = if s.mic_enabled { - egui::Stroke::new(1.0, t::BORDER_DEFAULT) - } else { - egui::Stroke::new(1.0, egui::Color32::from_rgba_premultiplied(217, 96, 74, 80)) - }; - let (mic_rect, mic_resp) = - ui.allocate_exact_size(egui::vec2(44.0, 44.0), egui::Sense::click()); - if ui.is_rect_visible(mic_rect) { - let painter = ui.painter(); - painter.rect_filled(mic_rect, t::R_FULL, mic_fill); - painter.rect_stroke(mic_rect, t::R_FULL, mic_stroke, egui::StrokeKind::Middle); - let mic_icon = if s.mic_enabled { - icons::microphone - } else { - icons::microphone_off - }; - let inner = inset(mic_rect, 10.0); - mic_icon(painter, inner, mic_color); - } - if mic_resp.clicked() { - s.mic_enabled = !s.mic_enabled; - } - mic_resp.on_hover_text("microphone"); - - // Deafen button — drawn icon. - let deaf_color = if s.deafen { - t::ERROR - } else { - t::TEXT_SECONDARY - }; - let deaf_fill = if s.deafen { - egui::Color32::from_rgba_premultiplied(217, 96, 74, 20) - } else { - t::BG_ELEVATED - }; - let (deaf_rect, deaf_resp) = - ui.allocate_exact_size(egui::vec2(44.0, 44.0), egui::Sense::click()); - if ui.is_rect_visible(deaf_rect) { - let painter = ui.painter(); - painter.rect_filled(deaf_rect, t::R_FULL, deaf_fill); - painter.rect_stroke( - deaf_rect, - t::R_FULL, - egui::Stroke::new(1.0, t::BORDER_DEFAULT), - egui::StrokeKind::Middle, - ); - let deaf_icon = if s.deafen { - icons::headphones_off - } else { - icons::headphones - }; - let inner = inset(deaf_rect, 10.0); - deaf_icon(painter, inner, deaf_color); - } - if deaf_resp.clicked() { - s.deafen = !s.deafen; - } - deaf_resp.on_hover_text("deafen"); - - // Leave button — drawn X on red background. - let (leave_rect, leave_resp) = - ui.allocate_exact_size(egui::vec2(44.0, 44.0), egui::Sense::click()); - if ui.is_rect_visible(leave_rect) { - let painter = ui.painter(); - painter.rect_filled(leave_rect, t::R_FULL, t::ERROR); - let inner = inset(leave_rect, 12.0); - icons::close(painter, inner, egui::Color32::from_rgb(26, 16, 8)); - } - if leave_resp.clicked() - && let Some(id) = s.active_channel.clone() - { - if s.channels.iter().any(|c| c.id == id && c.kind == "text") { - s.active_channel = Some("general".into()); - } else { - s.active_channel = s - .channels - .iter() - .find(|c| c.kind == "text") - .map(|c| c.id.clone()); - } - } - leave_resp.on_hover_text("leave voice channel"); - }); -} - -fn inset(rect: Rect, pad: f32) -> Rect { - Rect::from_min_max( - (rect.min.x + pad, rect.min.y + pad).into(), - (rect.max.x - pad, rect.max.y - pad).into(), - ) -} - -fn nick_color(nick: &str) -> egui::Color32 { - let colors = [ - t::ACCENT, - t::SUCCESS, - t::WARNING, - t::INFO, - egui::Color32::from_rgb(200, 100, 180), - ]; - colors[nick.bytes().fold(0usize, |a, b| a.wrapping_add(b as usize)) % colors.len()] -} diff --git a/src/ui/connect/action.rs b/src/ui/connect/action.rs deleted file mode 100644 index ea753be..0000000 --- a/src/ui/connect/action.rs +++ /dev/null @@ -1,45 +0,0 @@ -use eframe::egui::Color32; - -use crate::state::{ConnState, UiState}; -use crate::theme as t; -use vnox_client::net::{NetCommand, NetHandle}; - -pub fn disconnect(s: &mut UiState, net: &NetHandle) { - let net = net.clone(); - tokio::spawn(async move { - net.send(NetCommand::Disconnect).await; - }); - s.conn = ConnState::Disconnected; -} - -pub fn connect_to(s: &mut UiState, net: &NetHandle, addr: &str) { - if addr.is_empty() { - return; - } - if let Some(i) = s.bookmarks.iter().position(|b| b.address == addr) { - s.selected_bookmark = i; - } - s.connect_input = addr.to_string(); - s.conn = ConnState::Connecting; - let net = net.clone(); - let address = addr.to_string(); - let auto_reconnect = s.auto_reconnect; - tokio::spawn(async move { - net.send(NetCommand::Connect { - address, - auto_reconnect, - }) - .await; - }); -} - -pub(crate) fn msg_color(msg: &str) -> Color32 { - let l = msg.to_ascii_lowercase(); - if l.contains("error") || l.contains("fail") || l.contains("refuse") { - t::DANGER - } else if l.contains("connected") { - t::SUCCESS - } else { - t::TEXT_MUTED - } -} diff --git a/src/ui/connect/bookmarks.rs b/src/ui/connect/bookmarks.rs deleted file mode 100644 index 54fc28d..0000000 --- a/src/ui/connect/bookmarks.rs +++ /dev/null @@ -1,130 +0,0 @@ -mod add; - -use eframe::egui::{self, Frame}; -use vnox_client::net::NetHandle; - -use crate::state::{ConnState, UiState}; -use crate::theme as t; - -use super::action::connect_to; - -pub fn server_list(ui: &mut egui::Ui, s: &mut UiState, net: &NetHandle) { - Frame::NONE - .fill(t::BG_ELEVATED) - .corner_radius(t::R_LG) - .stroke(egui::Stroke::new(1.0, t::BORDER_DEFAULT)) - .inner_margin(egui::Margin::same(16)) - .show(ui, |ui| { - ui.set_width(ui.available_width()); - ui.horizontal(|ui| { - ui.label( - egui::RichText::new("SERVERS") - .font(egui::FontId::proportional(11.0)) - .color(t::TEXT_MUTED), - ); - ui.with_layout(egui::Layout::right_to_left(egui::Align::Center), |ui| { - let add_btn = egui::Button::new( - egui::RichText::new("+") - .font(egui::FontId::proportional(16.0)) - .color(t::ACCENT_SOFT), - ) - .fill(t::ACCENT_10) - .stroke(egui::Stroke::new(1.0, t::BORDER_ACCENT)) - .corner_radius(t::R_SM) - .min_size(egui::vec2(28.0, 28.0)); - if ui.add(add_btn).on_hover_text("add server").clicked() { - s.add_server_open = !s.add_server_open; - if s.add_server_open { - s.add_server_input = String::new(); - } - } - }); - }); - if s.add_server_open { - add::add_form(ui, s); - } - ui.add_space(4.0); - let busy = matches!( - s.conn, - ConnState::Connecting | ConnState::Reconnecting { .. } - ); - - if s.bookmarks.is_empty() { - ui.add_space(12.0); - ui.vertical_centered(|ui| { - ui.label( - egui::RichText::new("no servers yet — click + to add one") - .font(egui::FontId::proportional(12.0)) - .color(t::TEXT_DISABLED), - ); - }); - ui.add_space(12.0); - } else { - for (i, bm) in s.bookmarks.clone().into_iter().enumerate() { - let active = s.connect_input == bm.address; - let bg = if active { - t::ACCENT_10 - } else { - egui::Color32::TRANSPARENT - }; - let stroke = if active { - egui::Stroke::new(1.0, t::BORDER_ACCENT) - } else { - egui::Stroke::NONE - }; - - let resp = Frame::NONE - .fill(bg) - .stroke(stroke) - .corner_radius(t::R_SM) - .inner_margin(egui::Margin::symmetric(8, 4)) - .show(ui, |ui| { - ui.horizontal(|ui| { - ui.label( - egui::RichText::new("🖥").font(egui::FontId::proportional(13.0)), - ); - ui.add_space(6.0); - ui.vertical(|ui| { - ui.label( - egui::RichText::new(&bm.label) - .font(egui::FontId::proportional(13.0)) - .color(t::TEXT_PRIMARY), - ); - ui.label( - egui::RichText::new(&bm.address) - .font(egui::FontId::monospace(10.0)) - .color(t::TEXT_MUTED), - ); - }); - ui.with_layout( - egui::Layout::right_to_left(egui::Align::Center), - |ui| { - if t::icon_btn_drawn( - ui, - crate::theme::icons::close, - "remove bookmark", - None, - 16.0, - ) - .clicked() - { - s.remove_bookmark(i); - } - }, - ); - }); - }) - .response - .interact(egui::Sense::click()); - - if resp.clicked() && !busy { - s.connect_input = bm.address.clone(); - s.selected_bookmark = i; - if matches!(s.conn, ConnState::Disconnected) { - connect_to(s, net, &bm.address); - } - } - } - } - }); -} diff --git a/src/ui/connect/bookmarks/add.rs b/src/ui/connect/bookmarks/add.rs deleted file mode 100644 index 41a3110..0000000 --- a/src/ui/connect/bookmarks/add.rs +++ /dev/null @@ -1,51 +0,0 @@ -use crate::state::UiState; -use crate::theme as t; -use eframe::egui::{self, Frame}; - -pub fn add_form(ui: &mut egui::Ui, s: &mut UiState) { - ui.add_space(8.0); - let mut addr = s.add_server_input.clone(); - Frame::NONE - .fill(t::BG_INTERACTIVE) - .corner_radius(t::R_SM) - .stroke(egui::Stroke::new(1.0, t::BORDER_ACCENT)) - .inner_margin(egui::Margin::symmetric(10, 6)) - .show(ui, |ui| { - ui.horizontal(|ui| { - ui.add( - egui::TextEdit::singleline(&mut addr) - .font(egui::FontId::monospace(12.0)) - .text_color(t::TEXT_PRIMARY) - .hint_text( - egui::RichText::new("ip:port or host:port").color(t::TEXT_DISABLED), - ) - .desired_width(ui.available_width() - 120.0) - .frame(false), - ); - let save_btn = egui::Button::new( - egui::RichText::new("Save") - .font(egui::FontId::proportional(11.0)) - .color(t::TEXT_PRIMARY) - .strong(), - ) - .fill(t::BG_INTERACTIVE) - .stroke(egui::Stroke::new(1.0, t::BORDER_ACCENT)) - .corner_radius(t::R_SM) - .min_size(egui::vec2(50.0, 24.0)); - if ui.add(save_btn).clicked() && !addr.trim().is_empty() { - s.connect_input = addr.trim().to_string(); - s.add_bookmark_from_input(); - s.add_server_open = false; - s.add_server_input = String::new(); - s.connect_input = String::new(); - } - if t::icon_btn_drawn(ui, crate::theme::icons::close, "cancel", None, 18.0).clicked() - { - s.add_server_open = false; - s.add_server_input = String::new(); - } - }); - }); - s.add_server_input = addr; - ui.add_space(6.0); -} diff --git a/src/ui/connect/mod.rs b/src/ui/connect/mod.rs deleted file mode 100644 index 7e96c1f..0000000 --- a/src/ui/connect/mod.rs +++ /dev/null @@ -1,149 +0,0 @@ -use eframe::egui::{self, Frame}; -use vnox_client::{identity::Identity, net::NetHandle}; - -use crate::state::{ConnState, UiState}; -use crate::theme as t; - -mod action; -mod bookmarks; - -pub use action::{connect_to, disconnect}; - -pub fn show(ui: &mut egui::Ui, s: &mut UiState, _identity: &Identity, net: &NetHandle) { - ui.vertical_centered(|ui| { - ui.set_max_width(520.0); - ui.add_space(40.0); - - ui.label( - egui::RichText::new("VNOX") - .font(egui::FontId::proportional(32.0)) - .color(t::TEXT_PRIMARY) - .strong(), - ); - ui.add_space(2.0); - ui.label( - egui::RichText::new("self-hosted voice & chat") - .font(egui::FontId::proportional(13.0)) - .color(t::TEXT_MUTED), - ); - ui.add_space(32.0); - - bookmarks::server_list(ui, s, net); - - ui.add_space(20.0); - - ui.label( - egui::RichText::new("Quick Connect") - .font(egui::FontId::proportional(11.0)) - .color(t::TEXT_MUTED), - ); - ui.add_space(6.0); - - Frame::NONE - .fill(t::BG_INTERACTIVE) - .corner_radius(t::R_MD) - .stroke(egui::Stroke::new(1.0, t::BORDER_DEFAULT)) - .inner_margin(egui::Margin::symmetric(12, 8)) - .show(ui, |ui| { - ui.horizontal(|ui| { - ui.label( - egui::RichText::new(">") - .font(egui::FontId::monospace(12.0)) - .color(t::ACCENT_SOFT), - ); - let w = ui.available_width().max(80.0); - ui.add( - egui::TextEdit::singleline(&mut s.connect_input) - .font(egui::FontId::monospace(13.0)) - .text_color(t::TEXT_PRIMARY) - .frame(false) - .hint_text( - egui::RichText::new("127.0.0.1:7600").color(t::TEXT_DISABLED), - ) - .desired_width(w), - ); - }); - }); - - let enter_pressed = ui.input(|i| i.key_pressed(egui::Key::Enter)); - ui.add_space(8.0); - - let label = match &s.conn { - ConnState::Disconnected => "Connect", - ConnState::Connecting | ConnState::Reconnecting { .. } => "Cancel", - ConnState::Connected { .. } => "Disconnect", - }; - let btn = egui::Button::new( - egui::RichText::new(label) - .font(egui::FontId::proportional(13.0)) - .color(t::TEXT_PRIMARY) - .strong(), - ) - .fill(t::BG_ELEVATED) - .stroke(egui::Stroke::new(1.0, t::BORDER_ACCENT)) - .corner_radius(t::R_MD) - .min_size(egui::vec2(200.0, 38.0)); - - if ui.add(btn).clicked() || enter_pressed { - match &s.conn { - ConnState::Disconnected => { - let addr = s.connect_input.trim().to_owned(); - if !addr.is_empty() { - connect_to(s, net, &addr); - } - } - _ => disconnect(s, net), - } - } - - ui.add_space(6.0); - ui.horizontal(|ui| { - ui.checkbox(&mut s.auto_reconnect, ""); - ui.label( - egui::RichText::new("auto-reconnect on drop") - .font(egui::FontId::proportional(12.0)) - .color(t::TEXT_MUTED), - ); - }); - - if let Some(ref msg) = s.status_msg { - ui.add_space(8.0); - ui.label( - egui::RichText::new(msg) - .font(egui::FontId::proportional(11.0)) - .color(action::msg_color(msg)), - ); - } - - ui.add_space(28.0); - let sep = ui.available_rect_before_wrap(); - let painter = ui.painter(); - painter.hline( - sep.x_range(), - sep.top(), - egui::Stroke::new(1.0, t::BORDER_SUBTLE), - ); - ui.add_space(12.0); - - ui.horizontal(|ui| { - ui.label(egui::RichText::new("💡").font(egui::FontId::proportional(13.0))); - ui.label( - egui::RichText::new("Host your own —") - .font(egui::FontId::proportional(12.0)) - .color(t::TEXT_MUTED), - ); - ui.label( - egui::RichText::new("github.com/loki5512344/Vnox") - .font(egui::FontId::proportional(12.0)) - .color(t::ACCENT_SOFT), - ); - ui.label( - egui::RichText::new(" · self-host in 5 min") - .font(egui::FontId::proportional(12.0)) - .color(t::TEXT_DISABLED), - ); - }); - - ui.add_space(20.0); - }); -} diff --git a/src/ui/keybind/bindings.rs b/src/ui/keybind/bindings.rs deleted file mode 100644 index ad49ea8..0000000 --- a/src/ui/keybind/bindings.rs +++ /dev/null @@ -1,133 +0,0 @@ -use eframe::egui::Key; - -pub(crate) fn name_from_key(key: Key) -> Option<&'static str> { - match key { - Key::Space => Some("space"), - Key::A => Some("a"), - Key::B => Some("b"), - Key::C => Some("c"), - Key::D => Some("d"), - Key::E => Some("e"), - Key::F => Some("f"), - Key::G => Some("g"), - Key::H => Some("h"), - Key::I => Some("i"), - Key::J => Some("j"), - Key::K => Some("k"), - Key::L => Some("l"), - Key::M => Some("m"), - Key::N => Some("n"), - Key::O => Some("o"), - Key::P => Some("p"), - Key::Q => Some("q"), - Key::R => Some("r"), - Key::S => Some("s"), - Key::T => Some("t"), - Key::U => Some("u"), - Key::V => Some("v"), - Key::W => Some("w"), - Key::X => Some("x"), - Key::Y => Some("y"), - Key::Z => Some("z"), - Key::F1 => Some("f1"), - Key::F2 => Some("f2"), - Key::F3 => Some("f3"), - Key::F4 => Some("f4"), - Key::F5 => Some("f5"), - Key::F6 => Some("f6"), - Key::F7 => Some("f7"), - Key::F8 => Some("f8"), - Key::F9 => Some("f9"), - Key::F10 => Some("f10"), - Key::F11 => Some("f11"), - Key::F12 => Some("f12"), - _ => None, - } -} - -pub(crate) fn key_from_name(name: &str) -> Option { - match name { - "a" => Some(Key::A), - "b" => Some(Key::B), - "c" => Some(Key::C), - "d" => Some(Key::D), - "e" => Some(Key::E), - "f" => Some(Key::F), - "g" => Some(Key::G), - "h" => Some(Key::H), - "i" => Some(Key::I), - "j" => Some(Key::J), - "k" => Some(Key::K), - "l" => Some(Key::L), - "m" => Some(Key::M), - "n" => Some(Key::N), - "o" => Some(Key::O), - "p" => Some(Key::P), - "q" => Some(Key::Q), - "r" => Some(Key::R), - "s" => Some(Key::S), - "t" => Some(Key::T), - "u" => Some(Key::U), - "v" => Some(Key::V), - "w" => Some(Key::W), - "x" => Some(Key::X), - "y" => Some(Key::Y), - "z" => Some(Key::Z), - "space" => Some(Key::Space), - "f1" => Some(Key::F1), - "f2" => Some(Key::F2), - "f3" => Some(Key::F3), - "f4" => Some(Key::F4), - "f5" => Some(Key::F5), - "f6" => Some(Key::F6), - "f7" => Some(Key::F7), - "f8" => Some(Key::F8), - "f9" => Some(Key::F9), - "f10" => Some(Key::F10), - "f11" => Some(Key::F11), - "f12" => Some(Key::F12), - _ => None, - } -} - -pub(crate) const ALL_KEYS: &[Key] = &[ - Key::A, - Key::B, - Key::C, - Key::D, - Key::E, - Key::F, - Key::G, - Key::H, - Key::I, - Key::J, - Key::K, - Key::L, - Key::M, - Key::N, - Key::O, - Key::P, - Key::Q, - Key::R, - Key::S, - Key::T, - Key::U, - Key::V, - Key::W, - Key::X, - Key::Y, - Key::Z, - Key::Space, - Key::F1, - Key::F2, - Key::F3, - Key::F4, - Key::F5, - Key::F6, - Key::F7, - Key::F8, - Key::F9, - Key::F10, - Key::F11, - Key::F12, -]; diff --git a/src/ui/keybind/mod.rs b/src/ui/keybind/mod.rs deleted file mode 100644 index 23dc066..0000000 --- a/src/ui/keybind/mod.rs +++ /dev/null @@ -1,114 +0,0 @@ -//! Global hotkey detection (egui input snapshot). - -mod bindings; - -use eframe::egui::{self, Key, PointerButton}; - -use bindings::{ALL_KEYS, key_from_name, name_from_key}; - -/// True while the binding is held (push-to-talk). -pub fn binding_held(ctx: &egui::Context, binding: &str) -> bool { - let b = binding.trim().to_lowercase(); - ctx.input(|i| match b.as_str() { - "mouse4" | "mb4" | "x1" => i.pointer.button_down(PointerButton::Extra1), - "mouse5" | "mb5" | "x2" => i.pointer.button_down(PointerButton::Extra2), - "lmb" | "mouse1" => i.pointer.button_down(PointerButton::Primary), - "rmb" | "mouse2" => i.pointer.button_down(PointerButton::Secondary), - "mmb" | "mouse3" => i.pointer.button_down(PointerButton::Middle), - "space" => i.key_down(Key::Space), - _ => combo_down(&b, i), - }) -} - -/// Rising edge (toggle mute/deafen, etc.). -pub fn binding_pressed(ctx: &egui::Context, binding: &str) -> bool { - let b = binding.trim().to_lowercase(); - ctx.input(|i| match b.as_str() { - "mouse4" | "mb4" | "x1" => i.pointer.button_pressed(PointerButton::Extra1), - "mouse5" | "mb5" | "x2" => i.pointer.button_pressed(PointerButton::Extra2), - "ctrl+m" => i.modifiers.ctrl && i.key_pressed(Key::M), - "ctrl+d" => i.modifiers.ctrl && i.key_pressed(Key::D), - "ctrl+shift+o" => i.modifiers.ctrl && i.modifiers.shift && i.key_pressed(Key::O), - _ => combo_pressed(&b, i), - }) -} - -/// Listen for any key/mouse press and return a binding string like "ctrl+m" or "mouse4". -pub fn listen_pressed(ctx: &egui::Context) -> Option { - ctx.input(|i| { - for &key in ALL_KEYS { - if i.key_pressed(key) { - let mut parts: Vec<&str> = Vec::new(); - if i.modifiers.ctrl { - parts.push("ctrl"); - } - if i.modifiers.shift { - parts.push("shift"); - } - if i.modifiers.alt { - parts.push("alt"); - } - if let Some(name) = name_from_key(key) { - parts.push(name); - return Some(parts.join("+")); - } - } - } - if i.pointer.button_pressed(PointerButton::Extra1) { - return Some("mouse4".into()); - } - if i.pointer.button_pressed(PointerButton::Extra2) { - return Some("mouse5".into()); - } - if i.pointer.button_pressed(PointerButton::Middle) { - return Some("mouse3".into()); - } - None - }) -} - -fn combo_down(binding: &str, i: &egui::InputState) -> bool { - let Some((mods, key)) = parse_combo(binding) else { - return false; - }; - mods.ctrl == i.modifiers.ctrl - && mods.shift == i.modifiers.shift - && mods.alt == i.modifiers.alt - && i.key_down(key) -} - -fn combo_pressed(binding: &str, i: &egui::InputState) -> bool { - let Some((mods, key)) = parse_combo(binding) else { - return false; - }; - mods.ctrl == i.modifiers.ctrl - && mods.shift == i.modifiers.shift - && mods.alt == i.modifiers.alt - && i.key_pressed(key) -} - -#[derive(Default)] -struct Mods { - ctrl: bool, - shift: bool, - alt: bool, -} - -fn parse_combo(binding: &str) -> Option<(Mods, Key)> { - let parts: Vec<&str> = binding.split('+').map(str::trim).collect(); - if parts.is_empty() { - return None; - } - let mut mods = Mods::default(); - let key_name = *parts.last()?; - for p in &parts[..parts.len().saturating_sub(1)] { - match *p { - "ctrl" | "control" => mods.ctrl = true, - "shift" => mods.shift = true, - "alt" => mods.alt = true, - _ => {} - } - } - let key = key_from_name(key_name)?; - Some((mods, key)) -} diff --git a/src/ui/members/mod.rs b/src/ui/members/mod.rs deleted file mode 100644 index b779b07..0000000 --- a/src/ui/members/mod.rs +++ /dev/null @@ -1,161 +0,0 @@ -//! Right-hand member list panel — shows online/offline members -//! of the currently active channel. - -use eframe::egui::{ - self, Color32, Context, FontId, Frame, RichText, ScrollArea, SidePanel, Stroke, -}; - -use crate::state::{Channel, UiState}; -use crate::theme as t; - -const PANEL_WIDTH: f32 = 220.0; - -pub fn show(ctx: &Context, s: &mut UiState) { - // Only show for guild text channels (not DMs, not voice-only view, not home/friends). - let ch_id = match &s.active_channel { - Some(id) => id.clone(), - None => return, - }; - if s.active_guild_id.is_none() { - return; - } - if s.active_dm_id.is_some() { - return; - } - let channel = match s.channels.iter().find(|c| c.id == ch_id) { - Some(c) => c.clone(), - None => return, - }; - if channel.kind == "voice" { - return; - } - - SidePanel::right("members") - .exact_width(PANEL_WIDTH) - .resizable(false) - .frame(Frame::NONE.fill(t::BG_SURFACE)) - .show_separator_line(false) - .show(ctx, |ui| { - let r = ui.max_rect(); - ui.painter() - .vline(r.left(), r.y_range(), Stroke::new(1.0, t::BORDER_SUBTLE)); - - ui.with_layout(egui::Layout::top_down_justified(egui::Align::LEFT), |ui| { - ui.add_space(12.0); - render_section(ui, s, &channel, true); - ui.add_space(8.0); - render_section(ui, s, &channel, false); - }); - }); -} - -fn render_section(ui: &mut egui::Ui, s: &UiState, channel: &Channel, online: bool) { - // Members currently in this channel - let mut members: Vec<&str> = channel.members.iter().map(|m| s.nick_for(m)).collect(); - - // Add current user if not present - let self_nick = "you"; - if !members.contains(&self_nick) { - members.push(self_nick); - } - - // Sort alphabetically (case-insensitive) - members.sort_by_key(|a| a.to_lowercase()); - - let visible: Vec<&str> = if online { - members.clone() - } else { - Vec::new() // we don't track offline members per channel yet - }; - - if online && visible.is_empty() { - return; - } - - let header = if online { - format!("ONLINE — {}", visible.len()) - } else { - "OFFLINE — 0".to_string() - }; - - ui.horizontal(|ui| { - ui.add_space(12.0); - ui.label( - RichText::new(header) - .font(FontId::proportional(10.0)) - .color(t::TEXT_MUTED) - .strong(), - ); - }); - ui.add_space(4.0); - - ScrollArea::vertical() - .auto_shrink([false, true]) - .show(ui, |ui| { - for nick in &visible { - member_row(ui, nick, online); - } - }); -} - -fn member_row(ui: &mut egui::Ui, nick: &str, online: bool) { - let (dot_color, text_color) = if online { - (t::SUCCESS, t::TEXT_PRIMARY) - } else { - (t::TEXT_DISABLED, t::TEXT_DISABLED) - }; - - Frame::NONE - .inner_margin(egui::Margin { - left: 12, - right: 8, - top: 2, - bottom: 2, - }) - .show(ui, |ui| { - ui.horizontal(|ui| { - ui.spacing_mut().item_spacing.x = 8.0; - let init = nick - .chars() - .next() - .unwrap_or('?') - .to_uppercase() - .to_string(); - let avatar_color = if online { - nick_avatar_color(nick) - } else { - t::BG_ELEVATED - }; - let (r, _) = ui.allocate_exact_size(egui::vec2(24.0, 24.0), egui::Sense::hover()); - ui.painter().circle_filled(r.center(), 12.0, avatar_color); - ui.painter().text( - r.center(), - egui::Align2::CENTER_CENTER, - &init, - egui::FontId::proportional(11.0), - Color32::from_rgb(15, 15, 15), - ); - // Status dot - let (sd, _) = ui.allocate_exact_size(egui::vec2(8.0, 8.0), egui::Sense::hover()); - ui.painter().circle_filled(sd.center(), 4.0, dot_color); - - ui.label( - RichText::new(nick) - .font(FontId::proportional(12.0)) - .color(text_color), - ); - }); - }); -} - -fn nick_avatar_color(nick: &str) -> egui::Color32 { - let colors = [ - t::ACCENT, - t::SUCCESS, - t::WARNING, - t::INFO, - Color32::from_rgb(200, 100, 180), - Color32::from_rgb(120, 190, 180), - ]; - colors[nick.bytes().fold(0usize, |a, b| a.wrapping_add(b as usize)) % colors.len()] -} diff --git a/src/ui/mod.rs b/src/ui/mod.rs index 04dd215..594d63a 100644 --- a/src/ui/mod.rs +++ b/src/ui/mod.rs @@ -1,12 +1,4 @@ -mod app; -pub mod chat; -pub mod connect; -pub mod keybind; -pub mod members; -pub mod settings; -pub mod sidebar; +pub mod bridge; +pub mod slint; pub mod state; -pub mod theme; -pub mod titlebar; - -pub use app::VnoxApp; +pub mod update; diff --git a/src/ui/settings/mod.rs b/src/ui/settings/mod.rs deleted file mode 100644 index fec7a3b..0000000 --- a/src/ui/settings/mod.rs +++ /dev/null @@ -1,78 +0,0 @@ -mod nav; -mod network; -mod tabs; -mod widgets; - -use crate::state::{SettingsPage, UiState}; -use crate::theme as t; -use eframe::egui::{self, Align, Context, Frame, Layout, ScrollArea, Vec2, Window}; -use vnox_client::identity::Identity; - -pub use nav::show_nav; - -pub fn show_window(ctx: &Context, s: &mut UiState, identity: &Identity) { - let open_id = egui::Id::new("settings_open_flag"); - if !s.settings_open { - ctx.data_mut(|d| d.remove_temp::(open_id)); - return; - } - if ctx.data_mut(|d| d.get_temp::(open_id)) != Some(true) { - ctx.data_mut(|d| d.insert_temp(open_id, true)); - s.force_refresh_audio_devices(); - } - s.refresh_audio_devices(); - Window::new("Settings") - .collapsible(false) - .resizable(true) - .default_size([720.0, 520.0]) - .min_size([520.0, 380.0]) - .frame( - Frame::window(&ctx.style()) - .fill(t::BG_ELEVATED) - .stroke(egui::Stroke::new(1.0, t::BORDER_DEFAULT)) - .corner_radius(t::R_LG) - .shadow(egui::epaint::Shadow::NONE), - ) - .show(ctx, |ui| { - ui.visuals_mut().override_text_color = Some(t::TEXT_PRIMARY); - let panel_h = ui.available_height().max(200.0); - ui.horizontal_top(|ui| { - ui.allocate_ui_with_layout( - Vec2::new(168.0, panel_h), - Layout::top_down(Align::LEFT), - |ui| nav::nav_panel(ui, s), - ); - ui.separator(); - ui.allocate_ui_with_layout( - Vec2::new(ui.available_width().max(280.0), panel_h), - Layout::top_down(Align::LEFT), - |ui| { - ScrollArea::vertical() - .id_salt("settings_content") - .max_height(panel_h) - .auto_shrink([false, true]) - .show(ui, |ui| { - ui.set_min_width(320.0); - ui.add_space(12.0); - show_content(ui, s, identity); - ui.add_space(12.0); - }); - }, - ); - }); - }); -} - -pub fn show_content(ui: &mut egui::Ui, s: &mut UiState, id: &Identity) { - match &s.settings_page.clone() { - SettingsPage::Voice => tabs::voice(ui, s), - SettingsPage::AudioOutput => tabs::output(ui, s), - SettingsPage::Identity => tabs::show(ui, id, s), - SettingsPage::Network => network::network(ui, s), - SettingsPage::Overlay => network::overlay(ui, s), - SettingsPage::Appearance => tabs::appearance(ui, s), - SettingsPage::Keybinds => tabs::keybinds(ui, s), - SettingsPage::Plugins => tabs::plugins(ui), - SettingsPage::Advanced => tabs::advanced(ui, s), - } -} diff --git a/src/ui/settings/nav.rs b/src/ui/settings/nav.rs deleted file mode 100644 index baad353..0000000 --- a/src/ui/settings/nav.rs +++ /dev/null @@ -1,125 +0,0 @@ -use crate::state::{SettingsPage, UiState}; -use crate::theme as t; -use eframe::egui::{self, Align, Context, FontId, Frame, Layout, ScrollArea, Vec2}; - -use super::widgets; - -pub(super) fn nav_panel(ui: &mut egui::Ui, s: &mut UiState) { - Frame::NONE - .fill(t::BG_SURFACE) - .inner_margin(egui::Margin::symmetric(8, 12)) - .show(ui, |ui| { - ui.vertical(|ui| { - ScrollArea::vertical() - .id_salt("settings_nav") - .max_height((ui.available_height() - 30.0).max(80.0)) - .auto_shrink([false, true]) - .show(ui, |ui| { - nav_group(ui, s, "account", &[(SettingsPage::Identity, "identity")]); - nav_group( - ui, - s, - "audio", - &[ - (SettingsPage::Voice, "voice"), - (SettingsPage::AudioOutput, "audio output"), - ], - ); - nav_group( - ui, - s, - "network", - &[ - (SettingsPage::Network, "network"), - (SettingsPage::Overlay, "overlay"), - ], - ); - nav_group( - ui, - s, - "app", - &[ - (SettingsPage::Appearance, "appearance"), - (SettingsPage::Keybinds, "keybinds"), - (SettingsPage::Plugins, "plugins"), - ], - ); - nav_group(ui, s, "debug", &[(SettingsPage::Advanced, "advanced")]); - }); - if back_btn(ui) { - s.settings_open = false; - } - }); - }); -} - -pub fn show_nav(ctx: &Context, s: &mut UiState) { - egui::SidePanel::left("settings_nav") - .exact_width(160.0) - .resizable(false) - .frame(Frame::new().fill(t::BG_STRIP)) - .show(ctx, |ui| { - ui.style_mut().spacing.item_spacing = Vec2::new(0.0, 1.0); - ui.add_space(10.0); - nav_group(ui, s, "account", &[(SettingsPage::Identity, "identity")]); - nav_group( - ui, - s, - "audio", - &[ - (SettingsPage::Voice, "voice"), - (SettingsPage::AudioOutput, "audio output"), - ], - ); - nav_group( - ui, - s, - "network", - &[ - (SettingsPage::Network, "network"), - (SettingsPage::Overlay, "overlay"), - ], - ); - nav_group( - ui, - s, - "app", - &[ - (SettingsPage::Appearance, "appearance"), - (SettingsPage::Keybinds, "keybinds"), - (SettingsPage::Plugins, "plugins"), - ], - ); - nav_group(ui, s, "debug", &[(SettingsPage::Advanced, "advanced")]); - ui.with_layout(Layout::bottom_up(Align::LEFT), |ui| { - ui.add_space(8.0); - if back_btn(ui) { - s.settings_open = false; - } - }); - }); -} - -fn nav_group(ui: &mut egui::Ui, s: &mut UiState, section: &str, items: &[(SettingsPage, &str)]) { - ui.add_space(4.0); - widgets::nav_section(ui, section); - for (page, label) in items { - if widgets::nav_item(ui, &s.settings_page == page, label) { - s.settings_page = page.clone(); - } - } -} - -fn back_btn(ui: &mut egui::Ui) -> bool { - ui.add_sized( - [ui.available_width().max(120.0), 28.0], - egui::Button::new( - egui::RichText::new("← back") - .font(FontId::monospace(11.0)) - .color(t::TEXT_GHOST), - ) - .fill(egui::Color32::TRANSPARENT) - .frame(false), - ) - .clicked() -} diff --git a/src/ui/settings/network.rs b/src/ui/settings/network.rs deleted file mode 100644 index 2908e35..0000000 --- a/src/ui/settings/network.rs +++ /dev/null @@ -1,132 +0,0 @@ -use super::widgets::{badge, group_label, page_header, s_row, toggle}; -use crate::state::UiState; -use crate::theme as t; -use eframe::egui::{self, Align, FontId, Frame, Layout, Margin, RichText, Stroke}; - -pub fn network(ui: &mut egui::Ui, s: &mut UiState) { - page_header(ui, "network", "LNEx protocol · relay · connection"); - - // Live status card - Frame::new() - .fill(t::BG_STRIP) - .stroke(Stroke::new(1.0, t::BORDER_FAINT)) - .corner_radius(6u8) - .inner_margin(Margin::symmetric(12, 10)) - .show(ui, |ui| { - status_row(ui, "protocol", |ui| badge(ui, "LNEx v1", t::ACCENT_SOFT)); - status_row(ui, "voice transport", |ui| badge(ui, "UDP", t::BLUE)); - status_row(ui, "control transport", |ui| badge(ui, "TCP", t::WARN)); - status_row(ui, "encryption", |ui| { - badge(ui, "on · chacha20", t::SUCCESS) - }); - status_row(ui, "rtt", |ui| { - let rtt = s - .rtt_ms - .map(|ms| format!("{ms}ms")) - .unwrap_or_else(|| "—".into()); - let color = s - .rtt_ms - .map(|ms| { - if ms < 50 { - t::SUCCESS - } else if ms < 120 { - t::WARN - } else { - t::DANGER - } - }) - .unwrap_or(t::TEXT_GHOST); - ui.label( - RichText::new(rtt) - .font(FontId::monospace(10.0)) - .color(color), - ); - }); - status_row(ui, "packet loss", |ui| { - ui.label( - RichText::new("0.1%") - .font(FontId::monospace(10.0)) - .color(t::SUCCESS), - ); - }); - }); - ui.add_space(12.0); - - group_label(ui, "relay"); - s_row( - ui, - "use relay node", - Some("route through relay if direct fails"), - |ui| { - toggle(ui, &mut s.relay_enabled); - }, - ); - s_row(ui, "relay address", None, |ui| { - ui.add( - egui::TextEdit::singleline(&mut s.relay_address) - .font(FontId::monospace(10.0)) - .text_color(t::TEXT_MUTED) - .desired_width(140.0), - ); - }); - s_row( - ui, - "auto relay selection", - Some("pick nearest by latency"), - |ui| { - toggle(ui, &mut s.auto_relay); - }, - ); - ui.add_space(8.0); - - group_label(ui, "connection"); - s_row( - ui, - "auto reconnect", - Some("retry with backoff after disconnect"), - |ui| { - toggle(ui, &mut s.auto_reconnect); - }, - ); - ui.add_space(8.0); - - group_label(ui, "advanced"); - s_row(ui, "UDP port", None, |ui| { - ui.add( - egui::TextEdit::singleline(&mut s.udp_port) - .font(FontId::monospace(10.0)) - .text_color(t::TEXT_MUTED) - .desired_width(70.0), - ); - }); - s_row(ui, "force relay only", Some("disable direct p2p"), |ui| { - toggle(ui, &mut s.force_relay); - }); -} - -fn status_row(ui: &mut egui::Ui, key: &str, val: impl FnOnce(&mut egui::Ui)) { - ui.horizontal(|ui| { - ui.label( - RichText::new(key) - .font(FontId::monospace(10.0)) - .color(t::TEXT_DEAD), - ); - ui.with_layout(Layout::right_to_left(Align::Center), val); - }); - ui.add_space(4.0); -} - -pub fn overlay(ui: &mut egui::Ui, s: &mut UiState) { - page_header(ui, "overlay", "in-game HUD · speaking indicators"); - - group_label(ui, "general"); - s_row(ui, "enable overlay", None, |ui| { - toggle(ui, &mut s.overlay_enabled); - }); - s_row(ui, "show speaking users", None, |ui| { - toggle(ui, &mut s.overlay_speakers); - }); - s_row(ui, "show latency", None, |ui| { - toggle(ui, &mut s.overlay_latency); - }); -} diff --git a/src/ui/settings/tabs/app.rs b/src/ui/settings/tabs/app.rs deleted file mode 100644 index 9c0cceb..0000000 --- a/src/ui/settings/tabs/app.rs +++ /dev/null @@ -1,167 +0,0 @@ -use crate::state::UiState; -use crate::theme as t; -use crate::ui::settings::widgets::{ - badge, group_label, page_header, s_row, slider_row, small_select, toggle, -}; -use eframe::egui::{self, FontId, RichText}; - -pub fn appearance(ui: &mut egui::Ui, s: &mut UiState) { - page_header(ui, "appearance", "theme · font · ui scale"); - group_label(ui, "theme"); - s_row(ui, "color scheme", None, |ui| { - let mut v = 0usize; - small_select( - ui, - "scheme", - &mut v, - &["hifi dark", "terminal green", "void"], - ); - }); - s_row(ui, "ui scale", None, |ui| { - slider_row(ui, &mut s.ui_scale, 80.0, 130.0, |v| { - format!("{}%", v as u32) - }); - }); - s_row(ui, "font", None, |ui| { - let mut v = 0usize; - small_select( - ui, - "font", - &mut v, - &["IBM Plex Mono", "Geist Mono", "Fira Code"], - ); - }); -} - -pub fn plugins(ui: &mut egui::Ui) { - page_header(ui, "plugins", "installed · runtime: deno"); - group_label(ui, "installed"); - s_row(ui, "relay-switcher", Some("v0.1.2 · raven"), |ui| { - let mut v = true; - toggle(ui, &mut v); - }); - s_row(ui, "music-bot", Some("v0.3.0 · community"), |ui| { - let mut v = false; - toggle(ui, &mut v); - }); -} - -pub fn advanced(ui: &mut egui::Ui, s: &mut UiState) { - page_header(ui, "advanced", "debug · logging · protocol internals"); - group_label(ui, "logging"); - s_row(ui, "log level", None, |ui| { - small_select( - ui, - "log_level", - &mut s.log_level, - &["info", "debug", "trace"], - ); - }); - s_row(ui, "log voice packets", Some("verbose · high cpu"), |ui| { - toggle(ui, &mut s.log_voice_packets); - }); - ui.add_space(8.0); - group_label(ui, "protocol"); - s_row(ui, "LNEx version", None, |ui| { - badge(ui, "v1", t::ACCENT_SOFT); - }); - s_row(ui, "show packet stats", None, |ui| { - toggle(ui, &mut s.show_packet_stats); - }); - s_row( - ui, - "disable encryption", - Some("debug only · dangerous"), - |ui| { - ui.label( - RichText::new("!") - .font(FontId::monospace(10.0)) - .color(t::DANGER), - ); - ui.add_space(4.0); - toggle(ui, &mut s.disable_encryption); - }, - ); -} - -pub fn keybinds(ui: &mut egui::Ui, s: &mut UiState) { - page_header(ui, "keybinds", "global hotkeys"); - group_label(ui, "voice"); - kb_row( - ui, - "push-to-talk", - &mut s.kb_ptt, - &mut s.kb_listening, - "ptt", - ); - kb_row( - ui, - "mute toggle", - &mut s.kb_mute, - &mut s.kb_listening, - "mute", - ); - kb_row( - ui, - "deafen toggle", - &mut s.kb_deafen, - &mut s.kb_listening, - "deafen", - ); - ui.add_space(8.0); - group_label(ui, "overlay"); - kb_row( - ui, - "toggle overlay", - &mut s.kb_overlay, - &mut s.kb_listening, - "overlay", - ); -} - -fn kb_row( - ui: &mut egui::Ui, - label: &str, - val: &mut String, - listening: &mut Option, - listen_key: &str, -) { - s_row(ui, label, None, |ui| { - let is_listening = listening.as_deref() == Some(listen_key); - if is_listening { - if let Some(binding) = crate::keybind::listen_pressed(ui.ctx()) { - *val = binding; - *listening = None; - } - let btn = egui::Button::new( - egui::RichText::new("Listening… press a key") - .font(egui::FontId::monospace(10.0)) - .color(egui::Color32::WHITE), - ) - .fill(crate::theme::ACCENT) - .corner_radius(crate::theme::R_SM) - .min_size(egui::vec2(120.0, 20.0)); - if ui.add(btn).clicked() { - *listening = None; - } - } else { - ui.add( - egui::TextEdit::singleline(val) - .font(egui::FontId::monospace(10.0)) - .text_color(crate::theme::TEXT_MUTED) - .desired_width(90.0), - ); - let rec = egui::Button::new( - egui::RichText::new("●") - .font(egui::FontId::monospace(9.0)) - .color(crate::theme::ERROR), - ) - .fill(egui::Color32::TRANSPARENT) - .frame(false) - .min_size(egui::vec2(16.0, 16.0)); - if ui.add(rec).on_hover_text("record keybind").clicked() { - *listening = Some(listen_key.to_string()); - } - } - }); -} diff --git a/src/ui/settings/tabs/audio.rs b/src/ui/settings/tabs/audio.rs deleted file mode 100644 index 009e5e3..0000000 --- a/src/ui/settings/tabs/audio.rs +++ /dev/null @@ -1,150 +0,0 @@ -use crate::state::UiState; -use crate::theme as t; -use crate::ui::settings::widgets::{ - badge, device_meta_row, group_label, page_header, s_row, slider_row, small_select, - small_select_labels, toggle, -}; -use eframe::egui::{self, FontId, RichText, Stroke, Vec2}; -use vnox_client::audio::devices::DeviceLabel; -fn selected_meta(index: usize, metas: &[DeviceLabel]) -> Option<&DeviceLabel> { - if index == 0 { - return None; - } - metas.get(index.saturating_sub(1)) -} - -pub fn voice(ui: &mut egui::Ui, s: &mut UiState) { - page_header(ui, "voice", "input device, encoding, push-to-talk"); - group_label(ui, "input device"); - s_row(ui, "microphone", None, |ui| { - small_select_labels(ui, "mic_dev", &mut s.mic_device, &s.input_device_labels); - }); - device_meta_row(ui, selected_meta(s.mic_device, &s.input_device_metas)); - s_row(ui, "input volume", None, |ui| { - slider_row(ui, &mut s.input_volume, 0.0, 100.0, |v| { - format!("{}%", v as u32) - }); - }); - s_row(ui, "noise suppression", Some("rnnoise · cpu ~2%"), |ui| { - toggle(ui, &mut s.noise_suppress); - }); - s_row(ui, "echo cancellation", None, |ui| { - toggle(ui, &mut s.echo_cancel); - }); - ui.add_space(8.0); - group_label(ui, "activation"); - s_row(ui, "mode", None, |ui| { - small_select( - ui, - "vad_mode", - &mut s.vad_mode, - &["push-to-talk", "voice activity", "always on"], - ); - }); - if s.vad_mode == 1 { - s_row( - ui, - "vad threshold", - Some("voice activity sensitivity"), - |ui| { - slider_row(ui, &mut s.vad_threshold, 0.0, 100.0, |v| { - format!("{}%", v as u32) - }); - }, - ); - } - ui.add_space(8.0); - group_label(ui, "codec"); - s_row(ui, "encoder", Some("opus · low latency mode"), |ui| { - badge(ui, "opus", t::ACCENT_SOFT); - }); - s_row(ui, "bitrate", Some("opus 8–128 kbps"), |ui| { - ui.horizontal(|ui| { - slider_row(ui, &mut s.voice_bitrate, 8.0, 128.0, |v| { - format!("{}k", v as u32) - }); - let presets = [32, 64, 96, 128usize]; - for &p in &presets { - let pf = p as f32; - let selected = (s.voice_bitrate - pf).abs() < 1.0; - let resp = ui.add_sized( - Vec2::new(28.0, 20.0), - egui::Button::new( - RichText::new(format!("{}", p)) - .font(FontId::monospace(8.0)) - .color(if selected { t::ACCENT } else { t::TEXT_MUTED }), - ) - .fill(if selected { - egui::Color32::from_rgba_premultiplied( - t::ACCENT.r(), - t::ACCENT.g(), - t::ACCENT.b(), - 25, - ) - } else { - egui::Color32::from_rgb(23, 23, 23) - }) - .stroke(Stroke::new(1.0, t::BORDER_SUBTLE)) - .corner_radius(3u8), - ); - if resp.clicked() { - s.voice_bitrate = pf; - } - } - }); - let effective = (s.voice_bitrate as u32).clamp(8, 128); - ui.horizontal(|ui| { - ui.add_space(4.0); - ui.label( - RichText::new(format!("effective: {} kbps", effective)) - .font(FontId::monospace(9.0)) - .color(t::TEXT_DEAD), - ); - }); - }); - s_row( - ui, - "packet interval", - Some("lower = less latency, more cpu"), - |ui| { - small_select( - ui, - "pkt_interval", - &mut s.packet_interval, - &["10ms", "20ms", "40ms"], - ); - }, - ); -} - -pub fn output(ui: &mut egui::Ui, s: &mut UiState) { - page_header(ui, "audio output", "output device · volume · jitter buffer"); - group_label(ui, "output device"); - s_row(ui, "device", None, |ui| { - small_select_labels(ui, "out_dev", &mut s.output_device, &s.output_device_labels); - }); - device_meta_row(ui, selected_meta(s.output_device, &s.output_device_metas)); - s_row(ui, "output volume", None, |ui| { - slider_row(ui, &mut s.output_volume, 0.0, 100.0, |v| { - format!("{}%", v as u32) - }); - }); - ui.add_space(8.0); - group_label(ui, "jitter buffer"); - s_row( - ui, - "buffer size", - Some("lower = less delay, more glitches"), - |ui| { - small_select(ui, "jitter", &mut s.jitter_size, &["20ms", "40ms", "80ms"]); - }, - ); - s_row( - ui, - "adaptive buffer", - Some("auto-adjust on packet loss"), - |ui| { - toggle(ui, &mut s.adaptive_buffer); - }, - ); -} diff --git a/src/ui/settings/tabs/identity.rs b/src/ui/settings/tabs/identity.rs deleted file mode 100644 index b3f189f..0000000 --- a/src/ui/settings/tabs/identity.rs +++ /dev/null @@ -1,629 +0,0 @@ -use crate::identity::{self, Identity}; -use crate::theme as t; -use crate::ui::settings::widgets::{group_label, id_button, page_header, s_row, toggle}; -use crate::ui::state::UiState; -use eframe::egui::{self, Color32, FontId, Frame, Margin, RichText, Stroke, Vec2}; - -pub fn show(ui: &mut egui::Ui, identity: &Identity, s: &mut UiState) { - page_header(ui, "identity", "your local keypair · no account required"); - Frame::new() - .fill(t::BG_PANEL) - .stroke(Stroke::new(1.0, t::BORDER)) - .corner_radius(7u8) - .inner_margin(Margin::same(12)) - .show(ui, |ui| { - ui.horizontal(|ui| { - let init: String = identity - .nickname - .chars() - .take(2) - .collect::() - .to_uppercase(); - let av = egui::Button::new( - RichText::new(&init) - .font(FontId::monospace(11.0)) - .color(t::ACCENT), - ) - .fill(Color32::from_rgba_premultiplied(255, 107, 53, 18)) - .stroke(Stroke::new( - 1.0, - Color32::from_rgba_premultiplied(255, 107, 53, 38), - )) - .corner_radius(8u8) - .min_size(Vec2::splat(36.0)); - ui.add_enabled(false, av); - ui.add_space(6.0); - ui.vertical(|ui| { - ui.label( - RichText::new(&identity.nickname) - .font(FontId::monospace(12.0)) - .color(t::TEXT_SECONDARY) - .strong(), - ); - ui.label( - RichText::new(format!("pub: {}…", &identity.pubkey_hex[..16])) - .font(FontId::monospace(9.0)) - .color(t::TEXT_GHOST), - ); - }); - }); - ui.add_space(10.0); - ui.horizontal_wrapped(|ui| { - ui.spacing_mut().item_spacing.x = 5.0; - if id_button(ui, "copy pubkey") { - ui.ctx().copy_text(identity.pubkey_hex.clone()); - } - if id_button(ui, "set passphrase") { - s.vault_set_open = true; - s.vault_passphrase_input.clear(); - s.vault_passphrase_confirm.clear(); - s.vault_error = None; - } - if id_button(ui, "remove passphrase") { - s.vault_remove_open = true; - s.vault_passphrase_input.clear(); - s.vault_error = None; - } - if id_button(ui, "export keyfile") { - s.export_keyfile_open = true; - s.export_keyfile_pass.clear(); - s.export_keyfile_confirm.clear(); - s.export_keyfile_output.clear(); - s.export_keyfile_error = None; - } - if id_button(ui, "import keyfile") { - s.import_keyfile_open = true; - s.import_keyfile_input.clear(); - s.import_keyfile_pass.clear(); - s.import_keyfile_error = None; - } - id_button(ui, "change nickname"); - id_button(ui, "rotate keypair"); - }); - }); - - // Vault status block. - ui.add_space(10.0); - group_label(ui, "vault status"); - let is_encrypted = identity::is_vault_encrypted(); - Frame::new() - .fill(if is_encrypted { t::BG_PANEL } else { t::BG_ELEVATED }) - .stroke(Stroke::new( - 1.0, - if is_encrypted { t::SUCCESS } else { t::BORDER }, - )) - .corner_radius(7u8) - .inner_margin(Margin::same(12)) - .show(ui, |ui| { - ui.horizontal(|ui| { - let (label, color) = if is_encrypted { - ("encrypted (Argon2id + ChaCha20-Poly1305)", t::SUCCESS) - } else { - ("plain JSON — passphrase not set", t::WARNING) - }; - // Drawn lock/unlock icon. - let (icon_rect, _) = ui.allocate_exact_size( - egui::vec2(18.0, 18.0), - egui::Sense::hover(), - ); - let icon_fn = if is_encrypted { - crate::theme::icons::lock - } else { - crate::theme::icons::unlock - }; - icon_fn(ui.painter(), icon_rect, color); - ui.label( - RichText::new(label) - .font(FontId::proportional(12.0)) - .color(color) - .strong(), - ); - }); - ui.add_space(4.0); - ui.label( - RichText::new(if is_encrypted { - "Your private key is encrypted at rest. The vault is unlocked in memory while VNOX is running." - } else { - "Anyone with filesystem access can read your private key. Set a passphrase to encrypt it." - }) - .font(FontId::proportional(11.0)) - .color(t::TEXT_MUTED), - ); - }); - - ui.add_space(10.0); - group_label(ui, "backup"); - s_row( - ui, - "seed phrase backup", - Some("24-word recovery · never share"), - |ui| { - let mut v = false; - toggle(ui, &mut v); - }, - ); - s_row( - ui, - "export to file", - Some(".vnox keyfile · encrypted"), - |ui| { - let _ = id_button(ui, "export"); - }, - ); - - ui.add_space(8.0); - group_label(ui, "permissions"); - s_row( - ui, - "show identity to strangers", - Some("pubkey visible on federated nodes"), - |ui| { - let mut v = true; - toggle(ui, &mut v); - }, - ); - - // Set-passphrase modal. - if s.vault_set_open { - show_set_passphrase_modal(ui.ctx(), identity, s); - } - // Remove-passphrase modal. - if s.vault_remove_open { - show_remove_passphrase_modal(ui.ctx(), identity, s); - } - // Export keyfile modal. - if s.export_keyfile_open { - show_export_keyfile_modal(ui.ctx(), identity, s); - } - // Import keyfile modal. - if s.import_keyfile_open { - show_import_keyfile_modal(ui.ctx(), s); - } -} - -fn show_set_passphrase_modal(ctx: &egui::Context, identity: &Identity, s: &mut UiState) { - egui::Window::new("Set Identity Passphrase") - .collapsible(false) - .resizable(false) - .anchor(egui::Align2::CENTER_CENTER, [0.0, 0.0]) - .frame( - Frame::window(&ctx.style()) - .fill(t::BG_ELEVATED) - .stroke(egui::Stroke::new(1.0, t::BORDER_DEFAULT)) - .corner_radius(t::R_LG), - ) - .show(ctx, |ui| { - ui.set_width(360.0); - ui.label( - RichText::new("Pick a strong passphrase. It will be used to encrypt your identity keypair at rest (Argon2id + ChaCha20-Poly1305).") - .font(FontId::proportional(11.0)) - .color(t::TEXT_MUTED), - ); - ui.add_space(4.0); - ui.label( - RichText::new("! There is no recovery — if you forget the passphrase, the keypair is lost.") - .font(FontId::proportional(11.0)) - .color(t::WARNING), - ); - ui.add_space(10.0); - ui.label(RichText::new("Passphrase").font(FontId::proportional(12.0)).color(t::TEXT_PRIMARY)); - let p1 = ui.add( - egui::TextEdit::singleline(&mut s.vault_passphrase_input) - .password(true) - .font(FontId::monospace(12.0)) - .desired_width(320.0), - ); - ui.add_space(6.0); - ui.label(RichText::new("Confirm").font(FontId::proportional(12.0)).color(t::TEXT_PRIMARY)); - let p2 = ui.add( - egui::TextEdit::singleline(&mut s.vault_passphrase_confirm) - .password(true) - .font(FontId::monospace(12.0)) - .desired_width(320.0), - ); - ui.add_space(10.0); - let mismatch = !s.vault_passphrase_input.is_empty() - && s.vault_passphrase_input != s.vault_passphrase_confirm; - if mismatch { - ui.label( - RichText::new("passphrases do not match") - .font(FontId::proportional(11.0)) - .color(t::ERROR), - ); - } - if s.vault_passphrase_input.len() < 8 && !s.vault_passphrase_input.is_empty() { - ui.label( - RichText::new("! use at least 8 characters") - .font(FontId::proportional(11.0)) - .color(t::WARNING), - ); - } - ui.add_space(8.0); - ui.horizontal(|ui| { - if ui - .add( - egui::Button::new( - RichText::new("Cancel") - .font(FontId::proportional(12.0)) - .color(t::TEXT_MUTED), - ) - .fill(egui::Color32::TRANSPARENT) - .frame(false), - ) - .clicked() - { - s.vault_set_open = false; - } - ui.with_layout(egui::Layout::right_to_left(egui::Align::Center), |ui| { - let can_apply = !s.vault_passphrase_input.is_empty() - && s.vault_passphrase_input == s.vault_passphrase_confirm - && s.vault_passphrase_input.len() >= 8; - let btn = egui::Button::new( - RichText::new("Encrypt & Save") - .font(FontId::proportional(12.0)) - .color(egui::Color32::WHITE), - ) - .fill(t::ACCENT) - .stroke(egui::Stroke::NONE) - .corner_radius(t::R_SM) - .min_size(egui::vec2(120.0, 28.0)); - if ui.add_enabled(can_apply, btn).clicked() { - match identity::save(identity, Some(&s.vault_passphrase_input)) { - Ok(()) => { - s.vault_set_open = false; - s.vault_passphrase_input.clear(); - s.vault_passphrase_confirm.clear(); - } - Err(e) => { - s.vault_error = Some(format!("save failed: {e}")); - } - } - } - }); - }); - if let Some(ref err) = s.vault_error { - ui.add_space(4.0); - ui.label(RichText::new(err).font(FontId::proportional(11.0)).color(t::ERROR)); - } - if (p1.lost_focus() || p2.lost_focus()) && ui.input(|i| i.key_pressed(egui::Key::Escape)) - { - s.vault_set_open = false; - } - }); -} - -fn show_remove_passphrase_modal(ctx: &egui::Context, identity: &Identity, s: &mut UiState) { - egui::Window::new("Remove Passphrase") - .collapsible(false) - .resizable(false) - .anchor(egui::Align2::CENTER_CENTER, [0.0, 0.0]) - .frame( - Frame::window(&ctx.style()) - .fill(t::BG_ELEVATED) - .stroke(egui::Stroke::new(1.0, t::BORDER_DEFAULT)) - .corner_radius(t::R_LG), - ) - .show(ctx, |ui| { - ui.set_width(360.0); - ui.label( - RichText::new("! This will store your identity as plain JSON on disk. Anyone with filesystem access will be able to read your private key.") - .font(FontId::proportional(11.0)) - .color(t::WARNING), - ); - ui.add_space(8.0); - ui.label( - RichText::new("Type REMOVE to confirm.") - .font(FontId::proportional(12.0)) - .color(t::TEXT_PRIMARY), - ); - ui.add_space(4.0); - let resp = ui.add( - egui::TextEdit::singleline(&mut s.vault_passphrase_input) - .font(FontId::monospace(12.0)) - .desired_width(320.0), - ); - ui.add_space(10.0); - ui.horizontal(|ui| { - if ui - .add( - egui::Button::new( - RichText::new("Cancel") - .font(FontId::proportional(12.0)) - .color(t::TEXT_MUTED), - ) - .fill(egui::Color32::TRANSPARENT) - .frame(false), - ) - .clicked() - { - s.vault_remove_open = false; - } - ui.with_layout(egui::Layout::right_to_left(egui::Align::Center), |ui| { - let can_apply = s.vault_passphrase_input == "REMOVE"; - let btn = egui::Button::new( - RichText::new("Remove Passphrase") - .font(FontId::proportional(12.0)) - .color(egui::Color32::WHITE), - ) - .fill(t::ERROR) - .stroke(egui::Stroke::NONE) - .corner_radius(t::R_SM) - .min_size(egui::vec2(140.0, 28.0)); - if ui.add_enabled(can_apply, btn).clicked() { - match identity::save(identity, None) { - Ok(()) => { - s.vault_remove_open = false; - s.vault_passphrase_input.clear(); - } - Err(e) => { - s.vault_error = Some(format!("save failed: {e}")); - } - } - } - }); - }); - if let Some(ref err) = s.vault_error { - ui.add_space(4.0); - ui.label(RichText::new(err).font(FontId::proportional(11.0)).color(t::ERROR)); - } - if resp.lost_focus() && ui.input(|i| i.key_pressed(egui::Key::Escape)) { - s.vault_remove_open = false; - } - }); -} - -fn show_export_keyfile_modal(ctx: &egui::Context, identity: &Identity, s: &mut UiState) { - egui::Window::new("Export Identity Keyfile") - .collapsible(false) - .resizable(true) - .default_size([440.0, 380.0]) - .anchor(egui::Align2::CENTER_CENTER, [0.0, 0.0]) - .frame( - Frame::window(&ctx.style()) - .fill(t::BG_ELEVATED) - .stroke(egui::Stroke::new(1.0, t::BORDER_DEFAULT)) - .corner_radius(t::R_LG), - ) - .show(ctx, |ui| { - ui.set_width(420.0); - ui.label( - RichText::new("Pick a passphrase to encrypt the keyfile. Anyone with the keyfile and the passphrase can impersonate you — share carefully.") - .font(FontId::proportional(11.0)) - .color(t::TEXT_MUTED), - ); - ui.add_space(8.0); - ui.label(RichText::new("Passphrase (optional — empty = plain JSON)").font(FontId::proportional(12.0)).color(t::TEXT_PRIMARY)); - ui.add( - egui::TextEdit::singleline(&mut s.export_keyfile_pass) - .password(true) - .font(FontId::monospace(12.0)) - .desired_width(380.0), - ); - ui.add_space(6.0); - ui.label(RichText::new("Confirm").font(FontId::proportional(12.0)).color(t::TEXT_PRIMARY)); - ui.add( - egui::TextEdit::singleline(&mut s.export_keyfile_confirm) - .password(true) - .font(FontId::monospace(12.0)) - .desired_width(380.0), - ); - let mismatch = !s.export_keyfile_pass.is_empty() - && s.export_keyfile_pass != s.export_keyfile_confirm; - if mismatch { - ui.label( - RichText::new("passphrases do not match") - .font(FontId::proportional(11.0)) - .color(t::ERROR), - ); - } - ui.add_space(8.0); - let can_generate = !mismatch; - let gen_btn = egui::Button::new( - RichText::new("Generate keyfile") - .font(FontId::proportional(12.0)) - .color(egui::Color32::WHITE), - ) - .fill(t::ACCENT) - .stroke(egui::Stroke::NONE) - .corner_radius(t::R_SM) - .min_size(egui::vec2(120.0, 28.0)); - if ui.add_enabled(can_generate, gen_btn).clicked() { - let pass = if s.export_keyfile_pass.is_empty() { - None - } else { - Some(s.export_keyfile_pass.as_str()) - }; - match identity::export_keyfile(identity, pass) { - Ok(text) => s.export_keyfile_output = text, - Err(e) => s.export_keyfile_error = Some(format!("export failed: {e}")), - } - } - if let Some(ref err) = s.export_keyfile_error { - ui.label(RichText::new(err).font(FontId::proportional(11.0)).color(t::ERROR)); - } - ui.add_space(6.0); - if !s.export_keyfile_output.is_empty() { - ui.label( - RichText::new("Keyfile (copy and save to a .vnoxkey file):") - .font(FontId::proportional(11.0)) - .color(t::TEXT_MUTED), - ); - egui::ScrollArea::vertical() - .max_height(160.0) - .show(ui, |ui| { - let mut buf = s.export_keyfile_output.clone(); - ui.add( - egui::TextEdit::multiline(&mut buf) - .font(FontId::monospace(10.0)) - .desired_width(380.0) - .interactive(true), - ); - }); - ui.add_space(4.0); - ui.horizontal(|ui| { - if ui - .add( - egui::Button::new( - RichText::new("Copy") - .font(FontId::proportional(11.0)) - .color(t::ACCENT), - ) - .fill(egui::Color32::TRANSPARENT) - .frame(false), - ) - .clicked() - { - ui.ctx().copy_text(s.export_keyfile_output.clone()); - } - if ui - .add( - egui::Button::new( - RichText::new("Save to disk") - .font(FontId::proportional(11.0)) - .color(t::ACCENT), - ) - .fill(egui::Color32::TRANSPARENT) - .frame(false), - ) - .clicked() - { - let path = dirs::document_dir() - .unwrap_or_else(|| std::path::PathBuf::from(".")) - .join(format!("vnox-{}-{}.vnoxkey", identity.nickname, &identity.pubkey_hex[..8])); - match std::fs::write(&path, &s.export_keyfile_output) { - Ok(()) => { - s.export_keyfile_error = Some(format!("saved to: {}", path.display())); - } - Err(e) => { - s.export_keyfile_error = Some(format!("save failed: {e}")); - } - } - } - }); - } - ui.add_space(8.0); - ui.horizontal(|ui| { - if ui - .add( - egui::Button::new( - RichText::new("Close") - .font(FontId::proportional(12.0)) - .color(t::TEXT_MUTED), - ) - .fill(egui::Color32::TRANSPARENT) - .frame(false), - ) - .clicked() - { - s.export_keyfile_open = false; - } - }); - }); -} - -fn show_import_keyfile_modal(ctx: &egui::Context, s: &mut UiState) { - egui::Window::new("Import Identity Keyfile") - .collapsible(false) - .resizable(true) - .default_size([440.0, 360.0]) - .anchor(egui::Align2::CENTER_CENTER, [0.0, 0.0]) - .frame( - Frame::window(&ctx.style()) - .fill(t::BG_ELEVATED) - .stroke(egui::Stroke::new(1.0, t::BORDER_DEFAULT)) - .corner_radius(t::R_LG), - ) - .show(ctx, |ui| { - ui.set_width(420.0); - ui.label( - RichText::new("! Importing a keyfile will replace your current identity. Back up your current keypair first if you want to keep it.") - .font(FontId::proportional(11.0)) - .color(t::WARNING), - ); - ui.add_space(8.0); - ui.label( - RichText::new("Paste keyfile JSON here:") - .font(FontId::proportional(12.0)) - .color(t::TEXT_PRIMARY), - ); - egui::ScrollArea::vertical() - .max_height(140.0) - .show(ui, |ui| { - ui.add( - egui::TextEdit::multiline(&mut s.import_keyfile_input) - .font(FontId::monospace(10.0)) - .desired_width(380.0) - .hint_text(RichText::new(r#"{"version":1,"scheme":"..."}"#).color(t::TEXT_DISABLED)), - ); - }); - ui.add_space(6.0); - ui.label( - RichText::new("Passphrase (leave empty for plain keyfiles)") - .font(FontId::proportional(12.0)) - .color(t::TEXT_PRIMARY), - ); - ui.add( - egui::TextEdit::singleline(&mut s.import_keyfile_pass) - .password(true) - .font(FontId::monospace(12.0)) - .desired_width(380.0), - ); - ui.add_space(8.0); - let can_import = !s.import_keyfile_input.trim().is_empty(); - let import_btn = egui::Button::new( - RichText::new("Import & Replace Identity") - .font(FontId::proportional(12.0)) - .color(egui::Color32::WHITE), - ) - .fill(t::ERROR) - .stroke(egui::Stroke::NONE) - .corner_radius(t::R_SM) - .min_size(egui::vec2(180.0, 28.0)); - if ui.add_enabled(can_import, import_btn).clicked() { - let pass = if s.import_keyfile_pass.is_empty() { - None - } else { - Some(s.import_keyfile_pass.as_str()) - }; - match identity::import_keyfile(s.import_keyfile_input.trim(), pass) { - Ok(identity) => { - // Persist imported identity (without passphrase for now — - // the user can set one separately). - match identity::save(&identity, None) { - Ok(()) => { - s.import_keyfile_error = Some( - "imported — restart VNOX to use the new identity".into(), - ); - } - Err(e) => { - s.import_keyfile_error = Some(format!("save failed: {e}")); - } - } - } - Err(e) => { - s.import_keyfile_error = Some(format!("import failed: {e}")); - } - } - } - if let Some(ref err) = s.import_keyfile_error { - ui.label(RichText::new(err).font(FontId::proportional(11.0)).color(t::TEXT_MUTED)); - } - ui.add_space(8.0); - ui.horizontal(|ui| { - if ui - .add( - egui::Button::new( - RichText::new("Close") - .font(FontId::proportional(12.0)) - .color(t::TEXT_MUTED), - ) - .fill(egui::Color32::TRANSPARENT) - .frame(false), - ) - .clicked() - { - s.import_keyfile_open = false; - } - }); - }); -} diff --git a/src/ui/settings/tabs/mod.rs b/src/ui/settings/tabs/mod.rs deleted file mode 100644 index 5118249..0000000 --- a/src/ui/settings/tabs/mod.rs +++ /dev/null @@ -1,7 +0,0 @@ -mod app; -mod audio; -mod identity; - -pub use app::{advanced, appearance, keybinds, plugins}; -pub use audio::{output, voice}; -pub use identity::show; diff --git a/src/ui/settings/widgets/controls.rs b/src/ui/settings/widgets/controls.rs deleted file mode 100644 index 0eceddd..0000000 --- a/src/ui/settings/widgets/controls.rs +++ /dev/null @@ -1,128 +0,0 @@ -use crate::theme as t; -use eframe::egui::{self, Color32, FontId, Frame, Margin, RichText, Sense, Stroke, Vec2}; - -pub fn toggle(ui: &mut egui::Ui, value: &mut bool) { - let (rect, resp) = ui.allocate_exact_size(Vec2::new(30.0, 16.0), Sense::click()); - if resp.clicked() { - *value = !*value; - } - let painter = ui.painter(); - let bg = if *value { - Color32::from_rgba_premultiplied(255, 107, 53, 38) - } else { - t::BG_ELEVATED - }; - let bdr = if *value { - Color32::from_rgb(100, 50, 25) - } else { - t::BORDER_SUBTLE - }; - painter.rect( - rect, - 8u8, - bg, - Stroke::new(1.0, bdr), - egui::StrokeKind::Inside, - ); - let kx = if *value { - rect.max.x - 7.0 - } else { - rect.min.x + 7.0 - }; - let kc = if *value { - t::ACCENT - } else { - Color32::from_rgb(45, 45, 45) - }; - painter.circle_filled(egui::pos2(kx, rect.center().y), 5.0, kc); -} - -pub fn small_select_labels(ui: &mut egui::Ui, id: &str, value: &mut usize, options: &[String]) { - let fallback = "?".to_string(); - let selected = options.get(*value).unwrap_or(&fallback); - egui::ComboBox::from_id_salt(id) - .selected_text( - RichText::new(selected.as_str()) - .font(FontId::monospace(10.0)) - .color(t::TEXT_MUTED), - ) - .width(220.0) - .show_ui(ui, |ui| { - for (i, opt) in options.iter().enumerate() { - ui.selectable_value( - value, - i, - RichText::new(opt.as_str()) - .font(FontId::monospace(10.0)) - .color(t::TEXT_SECONDARY), - ); - } - }); -} - -pub fn small_select(ui: &mut egui::Ui, id: &str, value: &mut usize, options: &[&str]) { - egui::ComboBox::from_id_salt(id) - .selected_text( - RichText::new(options.get(*value).copied().unwrap_or("?")) - .font(FontId::monospace(10.0)) - .color(t::TEXT_MUTED), - ) - .width(110.0) - .show_ui(ui, |ui| { - for (i, opt) in options.iter().enumerate() { - ui.selectable_value( - value, - i, - RichText::new(*opt) - .font(FontId::monospace(10.0)) - .color(t::TEXT_SECONDARY), - ); - } - }); -} - -pub fn slider_row( - ui: &mut egui::Ui, - val: &mut f32, - min: f32, - max: f32, - fmt: impl Fn(f32) -> String, -) { - ui.horizontal(|ui| { - ui.spacing_mut().slider_width = 90.0; - ui.add( - egui::Slider::new(val, min..=max) - .show_value(false) - .trailing_fill(true), - ); - ui.add_space(4.0); - ui.label( - RichText::new(fmt(*val)) - .font(FontId::monospace(10.0)) - .color(t::TEXT_DIM), - ); - }); -} - -pub fn badge(ui: &mut egui::Ui, text: &str, color: Color32) { - Frame::new() - .fill(Color32::from_rgba_premultiplied( - color.r(), - color.g(), - color.b(), - 25, - )) - .stroke(Stroke::new( - 1.0, - Color32::from_rgba_premultiplied(color.r(), color.g(), color.b(), 50), - )) - .corner_radius(3u8) - .inner_margin(Margin::symmetric(6, 2)) - .show(ui, |ui| { - ui.label( - RichText::new(text) - .font(FontId::monospace(9.0)) - .color(color), - ); - }); -} diff --git a/src/ui/settings/widgets/layout.rs b/src/ui/settings/widgets/layout.rs deleted file mode 100644 index 852f793..0000000 --- a/src/ui/settings/widgets/layout.rs +++ /dev/null @@ -1,90 +0,0 @@ -use crate::theme as t; -use eframe::egui::{self, Align, Color32, FontId, Layout, RichText, Stroke}; - -pub fn page_header(ui: &mut egui::Ui, title: &str, sub: &str) { - ui.label( - RichText::new(title) - .font(FontId::monospace(13.0)) - .color(t::TEXT_SECONDARY) - .strong(), - ); - ui.add_space(2.0); - ui.label( - RichText::new(sub) - .font(FontId::monospace(9.0)) - .color(t::TEXT_DEAD), - ); - ui.add_space(14.0); -} - -pub fn group_label(ui: &mut egui::Ui, label: &str) { - ui.add_space(2.0); - ui.label( - RichText::new(label.to_uppercase()) - .font(FontId::monospace(9.0)) - .color(t::TEXT_DEAD), - ); - ui.add(egui::Separator::default().spacing(6.0)); - ui.add_space(2.0); -} - -pub fn s_row( - ui: &mut egui::Ui, - label: &str, - desc: Option<&str>, - content: impl FnOnce(&mut egui::Ui), -) { - ui.horizontal(|ui| { - ui.vertical(|ui| { - ui.set_min_width(200.0); - ui.label( - RichText::new(label) - .font(FontId::monospace(11.0)) - .color(t::TEXT_DIM), - ); - if let Some(d) = desc { - ui.label( - RichText::new(d) - .font(FontId::monospace(9.0)) - .color(t::TEXT_DEAD), - ); - } - }); - ui.with_layout(Layout::right_to_left(Align::Center), content); - }); - ui.add_space(4.0); - ui.add(egui::Separator::default().horizontal().spacing(0.0)); - ui.add_space(4.0); -} - -pub fn device_meta_row(ui: &mut egui::Ui, meta: Option<&vnox_client::audio::devices::DeviceLabel>) { - if let Some(m) = meta { - ui.add_space(2.0); - ui.horizontal(|ui| { - ui.add_space(4.0); - super::controls::badge(ui, &m.host_api, Color32::from_rgb(120, 180, 255)); - if m.channels > 0 { - super::controls::badge( - ui, - &format!("{}ch", m.channels), - Color32::from_rgb(180, 200, 120), - ); - } - }); - ui.add_space(2.0); - } -} - -pub fn id_button(ui: &mut egui::Ui, label: &str) -> bool { - ui.add( - egui::Button::new( - RichText::new(label) - .font(FontId::monospace(9.0)) - .color(t::TEXT_MUTED), - ) - .fill(Color32::from_rgb(23, 23, 23)) - .stroke(Stroke::new(1.0, t::BORDER_SUBTLE)) - .corner_radius(4u8), - ) - .clicked() -} diff --git a/src/ui/settings/widgets/mod.rs b/src/ui/settings/widgets/mod.rs deleted file mode 100644 index 3882cd4..0000000 --- a/src/ui/settings/widgets/mod.rs +++ /dev/null @@ -1,7 +0,0 @@ -mod controls; -mod layout; -mod nav; - -pub use controls::{badge, slider_row, small_select, small_select_labels, toggle}; -pub use layout::{device_meta_row, group_label, id_button, page_header, s_row}; -pub use nav::{nav_item, nav_section}; diff --git a/src/ui/settings/widgets/nav.rs b/src/ui/settings/widgets/nav.rs deleted file mode 100644 index d782665..0000000 --- a/src/ui/settings/widgets/nav.rs +++ /dev/null @@ -1,46 +0,0 @@ -use crate::theme as t; -use eframe::egui::{self, FontId, RichText}; - -pub fn nav_item(ui: &mut egui::Ui, is_active: bool, label: &str) -> bool { - let text_color = if is_active { - t::ACCENT_SOFT - } else { - t::TEXT_GHOST - }; - let fill = if is_active { - t::ACCENT_10 - } else { - egui::Color32::TRANSPARENT - }; - let stroke = if is_active { - egui::Stroke::new(1.0, t::BORDER_ACCENT) - } else { - egui::Stroke::NONE - }; - - ui.add_sized( - [ui.available_width().max(120.0), 26.0], - egui::Button::new( - RichText::new(label) - .font(FontId::monospace(11.0)) - .color(text_color), - ) - .fill(fill) - .stroke(stroke) - .corner_radius(t::R_SM), - ) - .clicked() -} - -pub fn nav_section(ui: &mut egui::Ui, label: &str) { - ui.add_space(6.0); - ui.horizontal(|ui| { - ui.add_space(8.0); - ui.label( - RichText::new(label.to_uppercase()) - .font(FontId::monospace(9.0)) - .color(t::TEXT_DEAD), - ); - }); - ui.add_space(2.0); -} diff --git a/src/ui/sidebar/channel_list/items.rs b/src/ui/sidebar/channel_list/items.rs deleted file mode 100644 index 03e25e2..0000000 --- a/src/ui/sidebar/channel_list/items.rs +++ /dev/null @@ -1,142 +0,0 @@ -use eframe::egui::{self, Frame}; - -use crate::theme as t; -use vnox_client::net::{NetCommand, NetHandle}; - -/// Channel row with optional context menu for deletion. -/// When `net` is provided and the channel is not a default one, a "Delete channel" -/// entry appears in the right-click context menu. -pub(super) fn ch_row_with_menu( - ui: &mut egui::Ui, - icon: &str, - name: &str, - active: bool, - badge: Option<&str>, - live: bool, - net: Option<&NetHandle>, -) -> bool { - let bg = if active { - t::ACCENT_10 - } else { - egui::Color32::TRANSPARENT - }; - let stroke = if active { - egui::Stroke::new(1.0, t::BORDER_ACCENT) - } else { - egui::Stroke::NONE - }; - let text_c = if active { - t::ACCENT_SOFT - } else { - t::TEXT_MUTED - }; - - let resp = Frame::NONE - .fill(bg) - .stroke(stroke) - .corner_radius(t::R_SM) - .inner_margin(egui::Margin::symmetric(8, 3)) - .show(ui, |ui| { - ui.horizontal(|ui| { - ui.label( - egui::RichText::new(icon) - .color(if active { t::ACCENT } else { t::TEXT_MUTED }) - .size(13.0), - ); - ui.label(egui::RichText::new(name).color(text_c).size(13.0)); - ui.with_layout(egui::Layout::right_to_left(egui::Align::Center), |ui| { - if live { - let (r, _) = - ui.allocate_exact_size(egui::vec2(6.0, 6.0), egui::Sense::hover()); - ui.painter().circle_filled(r.center(), 3.0, t::SUCCESS); - } else if let Some(b) = badge { - ui.label( - egui::RichText::new(b) - .font(egui::FontId::proportional(10.0)) - .color(t::TEXT_DISABLED), - ); - } - }); - }); - }) - .response - .interact(egui::Sense::click()); - - // Right-click context menu — offer deletion for non-default channels. - if let Some(net) = net - && name != "general" - && name != "voice" - { - resp.context_menu(|ui| { - if ui - .add( - egui::Button::new( - egui::RichText::new("Delete channel") - .font(egui::FontId::proportional(12.0)) - .color(t::ERROR), - ) - .fill(egui::Color32::TRANSPARENT) - .frame(false), - ) - .clicked() - { - let net = net.clone(); - let ch_id = name.to_string(); - tokio::spawn(async move { - net.send(NetCommand::ChannelDelete { channel_id: ch_id }) - .await; - }); - ui.close(); - } - }); - } - - resp.clicked() -} - -pub(super) fn member_row(ui: &mut egui::Ui, nick: &str) { - Frame::NONE - .inner_margin(egui::Margin { - left: 28, - right: 8, - top: 4, - bottom: 4, - }) - .show(ui, |ui| { - ui.horizontal(|ui| { - ui.spacing_mut().item_spacing.x = 6.0; - let init = nick - .chars() - .next() - .unwrap_or('?') - .to_uppercase() - .to_string(); - let (r, _) = ui.allocate_exact_size(egui::vec2(16.0, 16.0), egui::Sense::hover()); - ui.painter() - .circle_filled(r.center(), 8.0, nick_color(nick)); - ui.painter().text( - r.center(), - egui::Align2::CENTER_CENTER, - &init, - egui::FontId::proportional(8.0), - egui::Color32::from_rgb(15, 15, 15), - ); - ui.label( - egui::RichText::new(nick) - .font(egui::FontId::proportional(11.5)) - .color(t::TEXT_MUTED), - ); - }); - }); -} - -fn nick_color(nick: &str) -> egui::Color32 { - let colors = [ - t::ACCENT, - t::SUCCESS, - t::WARNING, - t::INFO, - egui::Color32::from_rgb(200, 100, 180), - ]; - colors[nick.bytes().fold(0usize, |a, b| a.wrapping_add(b as usize)) % colors.len()] -} diff --git a/src/ui/sidebar/channel_list/mod.rs b/src/ui/sidebar/channel_list/mod.rs deleted file mode 100644 index 227f0dd..0000000 --- a/src/ui/sidebar/channel_list/mod.rs +++ /dev/null @@ -1,327 +0,0 @@ -mod items; - -use eframe::egui::{self, Frame}; -use vnox_client::net::{NetCommand, NetHandle}; - -use crate::state::{Channel, UiState, VoiceUiState}; -use crate::theme as t; - -pub fn show(ui: &mut egui::Ui, s: &mut UiState, net: &NetHandle) { - if matches!(s.conn, crate::state::ConnState::Connected { .. }) && s.channels.is_empty() { - s.seed_default_channels(); - if s.active_channel.is_none() { - s.active_channel = Some("general".into()); - } - } - - if let Some(guild_id) = &s.active_guild_id.clone() - && let Some(g) = s.guilds.iter().find(|g| g.guild_id == *guild_id) - { - ui.add_space(4.0); - ui.horizontal(|ui| { - ui.label( - egui::RichText::new(&g.name) - .font(egui::FontId::proportional(14.0)) - .color(t::TEXT_PRIMARY), - ); - ui.with_layout(egui::Layout::right_to_left(egui::Align::Center), |ui| { - Frame::NONE - .inner_margin(egui::Margin::symmetric(4, 2)) - .show(ui, |ui| { - ui.label( - egui::RichText::new(format!("{} members", g.member_count)) - .font(egui::FontId::proportional(10.0)) - .color(t::TEXT_MUTED), - ); - }); - // Create channel button ("+"). - if ui - .add( - egui::Button::new( - egui::RichText::new("+") - .font(egui::FontId::proportional(12.0)) - .color(t::TEXT_MUTED), - ) - .fill(t::BG_ELEVATED) - .stroke(egui::Stroke::new(1.0, t::BORDER_DEFAULT)) - .corner_radius(t::R_SM) - .min_size(egui::vec2(20.0, 18.0)), - ) - .on_hover_text("create a channel") - .clicked() - { - s.create_channel_open = true; - s.create_channel_input.clear(); - } - let guild_id = guild_id.clone(); - if ui - .add( - egui::Button::new( - egui::RichText::new("Leave") - .font(egui::FontId::proportional(10.0)) - .color(t::TEXT_MUTED), - ) - .fill(t::BG_ELEVATED) - .stroke(egui::Stroke::new(1.0, t::BORDER_DEFAULT)) - .corner_radius(t::R_SM), - ) - .on_hover_text("leave this guild") - .clicked() - { - s.active_guild_id = None; - let net = net.clone(); - tokio::spawn(async move { - net.send(NetCommand::GuildLeave { guild_id }).await; - }); - } - }); - }); - ui.add_space(2.0); - ui.separator(); - ui.add_space(4.0); - } else { - // Home (no guild) — show a "Create Channel" button in the channel header. - ui.add_space(4.0); - ui.horizontal(|ui| { - ui.label( - egui::RichText::new("CHANNELS") - .font(egui::FontId::proportional(10.0)) - .color(t::TEXT_MUTED) - .strong(), - ); - ui.with_layout(egui::Layout::right_to_left(egui::Align::Center), |ui| { - if ui - .add( - egui::Button::new( - egui::RichText::new("+") - .font(egui::FontId::proportional(12.0)) - .color(t::TEXT_MUTED), - ) - .fill(t::BG_ELEVATED) - .stroke(egui::Stroke::new(1.0, t::BORDER_DEFAULT)) - .corner_radius(t::R_SM) - .min_size(egui::vec2(20.0, 18.0)), - ) - .on_hover_text("create a channel") - .clicked() - { - s.create_channel_open = true; - s.create_channel_input.clear(); - } - }); - }); - ui.add_space(2.0); - ui.separator(); - ui.add_space(4.0); - } - - if s.create_channel_open { - show_create_channel_popup(ui.ctx(), s, net); - } - - let text_chs: Vec = s - .channels - .iter() - .filter(|c| c.kind == "text") - .cloned() - .collect(); - let voice_chs: Vec = s - .channels - .iter() - .filter(|c| c.kind == "voice") - .cloned() - .collect(); - - if !text_chs.is_empty() { - cat_label(ui, s, "text"); - if !s.collapsed_categories.contains("text") { - for ch in &text_chs { - let active = s.active_channel.as_deref() == Some(&ch.id); - if items::ch_row_with_menu(ui, "#", &ch.name, active, None, false, Some(net)) { - join(s, net, &ch.id); - } - } - } - } - - if !voice_chs.is_empty() { - ui.add_space(4.0); - cat_label(ui, s, "voice"); - if !s.collapsed_categories.contains("voice") { - for ch in &voice_chs { - let active = s.active_channel.as_deref() == Some(&ch.id); - let live = - matches!(&s.voice, VoiceUiState::Active { channel } if channel == &ch.id); - let badge = (!ch.members.is_empty()).then(|| ch.members.len().to_string()); - if items::ch_row_with_menu( - ui, - "~", - &ch.name, - active, - badge.as_deref(), - live, - Some(net), - ) { - join(s, net, &ch.id); - } - if live || ch.name == "lobby" { - for member in &ch.members { - items::member_row(ui, member); - } - } - } - } - } - - if s.channels.is_empty() { - ui.add_space(8.0); - ui.label( - egui::RichText::new("waiting for channels…") - .font(egui::FontId::proportional(11.0)) - .color(t::TEXT_DISABLED), - ); - } -} - -fn cat_label(ui: &mut egui::Ui, s: &mut UiState, text: &str) { - ui.add_space(2.0); - let collapsed = s.collapsed_categories.contains(text); - let arrow = if collapsed { "▶" } else { "▼" }; - let resp = Frame::NONE - .inner_margin(egui::Margin { - left: 12, - right: 8, - top: 2, - bottom: 2, - }) - .show(ui, |ui| { - ui.horizontal(|ui| { - ui.label( - egui::RichText::new(arrow) - .font(egui::FontId::proportional(8.0)) - .color(t::TEXT_MUTED), - ); - ui.label( - egui::RichText::new(text.to_uppercase()) - .font(egui::FontId::proportional(10.0)) - .color(t::TEXT_MUTED), - ); - }); - }) - .response - .interact(egui::Sense::click()); - if resp.clicked() { - if collapsed { - s.collapsed_categories.remove(text); - } else { - s.collapsed_categories.insert(text.to_string()); - } - } -} - -fn join(s: &mut UiState, net: &NetHandle, channel_id: &str) { - s.active_channel = Some(channel_id.to_string()); - s.server_channel = Some(channel_id.to_string()); - let net = net.clone(); - let id = channel_id.to_string(); - tokio::spawn(async move { - net.send(NetCommand::JoinChannel { channel_id: id }).await; - }); -} - -fn show_create_channel_popup(ctx: &egui::Context, s: &mut UiState, net: &NetHandle) { - egui::Window::new("Create a Channel") - .collapsible(false) - .resizable(false) - .anchor(egui::Align2::CENTER_CENTER, [0.0, 0.0]) - .frame( - Frame::window(&ctx.style()) - .fill(t::BG_ELEVATED) - .stroke(egui::Stroke::new(1.0, t::BORDER_DEFAULT)) - .corner_radius(t::R_LG), - ) - .show(ctx, |ui| { - ui.set_width(300.0); - ui.label( - egui::RichText::new("Channel Name") - .font(egui::FontId::proportional(12.0)) - .color(t::TEXT_PRIMARY), - ); - ui.add_space(8.0); - let resp = ui.add( - egui::TextEdit::singleline(&mut s.create_channel_input) - .font(egui::FontId::proportional(14.0)) - .text_color(t::TEXT_PRIMARY) - .hint_text(egui::RichText::new("e.g. dev-talk").color(t::TEXT_DISABLED)) - .desired_width(260.0), - ); - ui.add_space(8.0); - ui.label( - egui::RichText::new("Type") - .font(egui::FontId::proportional(12.0)) - .color(t::TEXT_PRIMARY), - ); - ui.add_space(4.0); - let kind_text = match s.create_channel_kind.as_str() { - "voice" => "Voice", - _ => "Text", - }; - egui::ComboBox::from_id_salt("channel_kind") - .selected_text(kind_text) - .show_ui(ui, |ui| { - ui.selectable_value(&mut s.create_channel_kind, "text".into(), "Text"); - ui.selectable_value(&mut s.create_channel_kind, "voice".into(), "Voice"); - }); - ui.add_space(12.0); - ui.horizontal(|ui| { - if ui - .add( - egui::Button::new( - egui::RichText::new("Cancel") - .font(egui::FontId::proportional(12.0)) - .color(t::TEXT_MUTED), - ) - .fill(egui::Color32::TRANSPARENT) - .frame(false), - ) - .clicked() - { - s.create_channel_open = false; - } - ui.with_layout(egui::Layout::right_to_left(egui::Align::Center), |ui| { - let create_btn = egui::Button::new( - egui::RichText::new("Create") - .font(egui::FontId::proportional(12.0)) - .color(egui::Color32::WHITE), - ) - .fill(t::ACCENT) - .stroke(egui::Stroke::NONE) - .corner_radius(t::R_SM) - .min_size(egui::vec2(80.0, 28.0)); - if ui.add(create_btn).clicked() && !s.create_channel_input.trim().is_empty() { - let name = s.create_channel_input.trim().to_string(); - let kind = s.create_channel_kind.clone(); - // Send server-side ChannelCreate — gateway will broadcast - // back a ChannelCreated event that updates the sidebar. - let net_clone = net.clone(); - let ch_id = name.clone(); - let ch_name = name.clone(); - let ch_kind = kind.clone(); - tokio::spawn(async move { - net_clone - .send(NetCommand::ChannelCreate { - channel_id: ch_id, - channel_name: ch_name, - kind: ch_kind, - }) - .await; - }); - s.create_channel_open = false; - } - }); - }); - if resp.lost_focus() && ui.input(|i| i.key_pressed(egui::Key::Escape)) { - s.create_channel_open = false; - } - }); -} diff --git a/src/ui/sidebar/guild_bar/icons.rs b/src/ui/sidebar/guild_bar/icons.rs deleted file mode 100644 index b2f429f..0000000 --- a/src/ui/sidebar/guild_bar/icons.rs +++ /dev/null @@ -1,82 +0,0 @@ -use eframe::egui::{self, FontId, Rect, RichText, Stroke}; - -use crate::theme as t; - -pub const GUILD_ICON_SIZE: f32 = 40.0; - -pub fn guild_icon( - ui: &mut egui::Ui, - label: &str, - tooltip: &str, - active: bool, - onclick: impl FnOnce(), -) { - let (bg, stroke) = if active { - (t::ACCENT_10, Stroke::new(1.5, t::ACCENT)) - } else { - (t::BG_ELEVATED, Stroke::new(1.0, t::BORDER_DEFAULT)) - }; - - let text_color = if active { - t::ACCENT_SOFT - } else { - t::TEXT_MUTED - }; - - let btn = egui::Button::new( - RichText::new(label) - .font(FontId::proportional(14.0)) - .color(text_color), - ) - .fill(bg) - .stroke(stroke) - .corner_radius(t::R_FULL) - .min_size(egui::vec2(GUILD_ICON_SIZE, GUILD_ICON_SIZE)); - - if ui.add(btn).on_hover_text(tooltip).clicked() { - onclick(); - } -} - -/// Guild icon that renders a vector-drawn glyph instead of a text label. -pub fn guild_icon_drawn( - ui: &mut egui::Ui, - draw_fn: fn(&egui::Painter, Rect, eframe::egui::Color32), - tooltip: &str, - active: bool, - onclick: impl FnOnce(), -) { - let (bg, stroke) = if active { - (t::ACCENT_10, Stroke::new(1.5, t::ACCENT)) - } else { - (t::BG_ELEVATED, Stroke::new(1.0, t::BORDER_DEFAULT)) - }; - let color = if active { - t::ACCENT_SOFT - } else { - t::TEXT_MUTED - }; - - let (rect, resp) = ui.allocate_exact_size( - egui::vec2(GUILD_ICON_SIZE, GUILD_ICON_SIZE), - egui::Sense::click(), - ); - if ui.is_rect_visible(rect) { - let painter = ui.painter(); - painter.rect_filled(rect, t::R_FULL, bg); - painter.rect_stroke(rect, t::R_FULL, stroke, egui::StrokeKind::Middle); - // Inset for the icon. - let pad = 8.0; - let inner = Rect::from_min_max( - (rect.min.x + pad, rect.min.y + pad).into(), - (rect.max.x - pad, rect.max.y - pad).into(), - ); - draw_fn(painter, inner, color); - } - if resp.clicked() { - onclick(); - } - if !tooltip.is_empty() { - resp.on_hover_text(tooltip); - } -} diff --git a/src/ui/sidebar/guild_bar/mod.rs b/src/ui/sidebar/guild_bar/mod.rs deleted file mode 100644 index 37f473b..0000000 --- a/src/ui/sidebar/guild_bar/mod.rs +++ /dev/null @@ -1,685 +0,0 @@ -use crate::theme::icons as vec_icons; -use crate::{state::UiState, theme as t}; -use eframe::egui::{self, FontId, Frame, RichText, Stroke}; -use vnox_client::net::{NetCommand, NetHandle}; - -mod icons; -const BAR_WIDTH: f32 = 56.0; - -pub fn show(ctx: &egui::Context, s: &mut UiState, net: &NetHandle) { - egui::SidePanel::left("guild_bar") - .exact_width(BAR_WIDTH) - .resizable(false) - .frame(Frame::NONE.fill(t::BG_STRIP)) - .show_separator_line(false) - .show(ctx, |ui| { - let r = ui.max_rect(); - ui.painter() - .vline(r.right(), r.y_range(), Stroke::new(1.0, t::BORDER_SUBTLE)); - - ui.with_layout( - egui::Layout::top_down_justified(egui::Align::Center), - |ui| { - ui.add_space(8.0); - - let home_active = s.active_guild_id.is_none(); - icons::guild_icon_drawn(ui, vec_icons::home, "Home", home_active, || { - s.active_guild_id = None; - s.active_dm_id = s.dm_conversations.first().map(|c| c.dm_id.clone()); - }); - ui.add_space(4.0); - - ui.painter().hline( - egui::Rangef::new(r.left() + 12.0, r.right() - 12.0), - ui.cursor().top() + 2.0, - Stroke::new(1.0, t::BORDER_SUBTLE), - ); - ui.add_space(6.0); - - for guild in &s.guilds { - let active = s.active_guild_id.as_deref() == Some(&guild.guild_id); - let initial = guild - .name - .chars() - .next() - .unwrap_or('?') - .to_uppercase() - .to_string(); - icons::guild_icon(ui, &initial, &guild.name, active, || { - s.active_guild_id = Some(guild.guild_id.clone()); - let net = net.clone(); - let gid = guild.guild_id.clone(); - tokio::spawn(async move { - net.send(vnox_client::net::NetCommand::GuildList).await; - let _ = gid; - }); - }); - ui.add_space(2.0); - } - - ui.add_space(4.0); - if t::icon_btn_drawn( - ui, - vec_icons::plus, - "create a guild", - None, - icons::GUILD_ICON_SIZE, - ) - .clicked() - { - s.create_guild_open = true; - s.create_guild_input.clear(); - } - - ui.add_space(4.0); - if t::icon_btn_drawn( - ui, - vec_icons::link, - "join with invite code", - None, - icons::GUILD_ICON_SIZE, - ) - .clicked() - { - s.invite_accept_open = true; - s.invite_accept_code.clear(); - } - - ui.add_space(4.0); - let has_guild = s.active_guild_id.is_some(); - let audit_resp = t::icon_btn_drawn( - ui, - vec_icons::clipboard, - "view audit log (admins only)", - None, - icons::GUILD_ICON_SIZE, - ); - if has_guild - && audit_resp.clicked() - && let Some(gid) = s.active_guild_id.clone() - { - s.audit_log_open = true; - s.audit_log_guild_id = Some(gid.clone()); - s.audit_log_entries.clear(); - let net = net.clone(); - tokio::spawn(async move { - net.send(NetCommand::GuildAuditLogFetch { - guild_id: gid, - limit: 50, - }) - .await; - }); - } - - ui.add_space(4.0); - let members_resp = t::icon_btn_drawn( - ui, - vec_icons::people, - "guild members", - None, - icons::GUILD_ICON_SIZE, - ); - let has_guild = s.active_guild_id.is_some(); - if has_guild - && members_resp.clicked() - && let Some(gid) = s.active_guild_id.clone() - { - s.guild_members_open = true; - s.guild_member_list_guild_id = Some(gid.clone()); - s.guild_member_list.clear(); - s.guild_role_list.clear(); - let net = net.clone(); - tokio::spawn(async move { - net.send(NetCommand::GuildMemberListFetch { - guild_id: gid.clone(), - }) - .await; - net.send(NetCommand::GuildRoleListFetch { guild_id: gid }) - .await; - }); - } - }, - ); - - if s.create_guild_open { - show_create_guild_popup(ctx, s, net); - } - if s.invite_accept_open { - show_invite_accept_popup(ctx, s, net); - } - if s.audit_log_open { - show_audit_log_popup(ctx, s, net); - } - if s.guild_members_open { - show_members_popup(ctx, s, net); - } - }); -} - -fn show_create_guild_popup(ctx: &egui::Context, s: &mut UiState, net: &NetHandle) { - egui::Window::new("Create a Guild") - .collapsible(false) - .resizable(false) - .anchor(egui::Align2::CENTER_CENTER, [0.0, 0.0]) - .frame( - Frame::window(&ctx.style()) - .fill(t::BG_ELEVATED) - .stroke(Stroke::new(1.0, t::BORDER_DEFAULT)) - .corner_radius(t::R_LG), - ) - .show(ctx, |ui| { - ui.set_width(280.0); - ui.label( - RichText::new("Guild Name") - .font(FontId::proportional(12.0)) - .color(t::TEXT_PRIMARY), - ); - ui.add_space(8.0); - let resp = ui.add( - egui::TextEdit::singleline(&mut s.create_guild_input) - .font(FontId::proportional(14.0)) - .text_color(t::TEXT_PRIMARY) - .hint_text(RichText::new("e.g. My Awesome Guild").color(t::TEXT_DISABLED)) - .desired_width(240.0), - ); - ui.add_space(12.0); - ui.horizontal(|ui| { - if ui - .add( - egui::Button::new( - RichText::new("Cancel") - .font(FontId::proportional(12.0)) - .color(t::TEXT_MUTED), - ) - .fill(egui::Color32::TRANSPARENT) - .frame(false), - ) - .clicked() - { - s.create_guild_open = false; - } - ui.with_layout(egui::Layout::right_to_left(egui::Align::Center), |ui| { - let create_btn = egui::Button::new( - RichText::new("Create") - .font(FontId::proportional(12.0)) - .color(egui::Color32::WHITE), - ) - .fill(t::ACCENT) - .stroke(Stroke::NONE) - .corner_radius(t::R_SM) - .min_size(egui::vec2(80.0, 28.0)); - if ui.add(create_btn).clicked() && !s.create_guild_input.trim().is_empty() { - let net = net.clone(); - let name = s.create_guild_input.trim().to_string(); - tokio::spawn(async move { - net.send(vnox_client::net::NetCommand::GuildCreate { name }) - .await; - }); - s.create_guild_open = false; - } - }); - }); - if resp.lost_focus() && ui.input(|i| i.key_pressed(egui::Key::Escape)) { - s.create_guild_open = false; - } - }); -} - -fn show_invite_accept_popup(ctx: &egui::Context, s: &mut UiState, net: &NetHandle) { - egui::Window::new("Join a Guild") - .collapsible(false) - .resizable(false) - .anchor(egui::Align2::CENTER_CENTER, [0.0, 0.0]) - .frame( - Frame::window(&ctx.style()) - .fill(t::BG_ELEVATED) - .stroke(Stroke::new(1.0, t::BORDER_DEFAULT)) - .corner_radius(t::R_LG), - ) - .show(ctx, |ui| { - ui.set_width(280.0); - ui.label( - RichText::new("Invite Code") - .font(FontId::proportional(12.0)) - .color(t::TEXT_PRIMARY), - ); - ui.add_space(8.0); - let resp = ui.add( - egui::TextEdit::singleline(&mut s.invite_accept_code) - .font(FontId::monospace(14.0)) - .text_color(t::TEXT_PRIMARY) - .hint_text(RichText::new("e.g. a1b2c3d4").color(t::TEXT_DISABLED)) - .desired_width(240.0), - ); - ui.add_space(12.0); - ui.horizontal(|ui| { - if ui - .add( - egui::Button::new( - RichText::new("Cancel") - .font(FontId::proportional(12.0)) - .color(t::TEXT_MUTED), - ) - .fill(egui::Color32::TRANSPARENT) - .frame(false), - ) - .clicked() - { - s.invite_accept_open = false; - } - ui.with_layout(egui::Layout::right_to_left(egui::Align::Center), |ui| { - let join_btn = egui::Button::new( - RichText::new("Join") - .font(FontId::proportional(12.0)) - .color(egui::Color32::WHITE), - ) - .fill(t::ACCENT) - .stroke(Stroke::NONE) - .corner_radius(t::R_SM) - .min_size(egui::vec2(80.0, 28.0)); - if ui.add(join_btn).clicked() && !s.invite_accept_code.trim().is_empty() { - let net = net.clone(); - let code = s.invite_accept_code.trim().to_string(); - tokio::spawn(async move { - net.send(NetCommand::InviteAccept { code }).await; - }); - s.invite_accept_open = false; - } - }); - }); - if resp.lost_focus() && ui.input(|i| i.key_pressed(egui::Key::Escape)) { - s.invite_accept_open = false; - } - }); -} - -fn show_audit_log_popup(ctx: &egui::Context, s: &mut UiState, _net: &NetHandle) { - egui::Window::new("Audit Log") - .collapsible(false) - .resizable(true) - .default_size([520.0, 420.0]) - .min_size([400.0, 300.0]) - .anchor(egui::Align2::CENTER_CENTER, [0.0, 0.0]) - .frame( - Frame::window(&ctx.style()) - .fill(t::BG_ELEVATED) - .stroke(egui::Stroke::new(1.0, t::BORDER_DEFAULT)) - .corner_radius(t::R_LG), - ) - .show(ctx, |ui| { - ui.set_width(500.0); - ui.horizontal(|ui| { - ui.label( - RichText::new("Last 50 admin actions for this guild") - .font(FontId::proportional(11.0)) - .color(t::TEXT_MUTED), - ); - ui.with_layout(egui::Layout::right_to_left(egui::Align::Center), |ui| { - if t::icon_btn_drawn(ui, vec_icons::close, "close", None, 18.0).clicked() { - s.audit_log_open = false; - } - }); - }); - ui.add_space(6.0); - ui.separator(); - ui.add_space(4.0); - - if s.audit_log_entries.is_empty() { - ui.vertical_centered(|ui| { - ui.add_space(40.0); - ui.label( - RichText::new("no audit log entries") - .font(FontId::proportional(12.0)) - .color(t::TEXT_DISABLED), - ); - ui.add_space(4.0); - ui.label( - RichText::new("(loading or empty — admin actions like kick, role change, invite create will appear here)") - .font(FontId::proportional(10.0)) - .color(t::TEXT_DISABLED), - ); - }); - return; - } - - egui::ScrollArea::vertical() - .auto_shrink([false, true]) - .show(ui, |ui| { - for entry in s.audit_log_entries.clone() { - Frame::NONE - .fill(t::BG_SURFACE) - .corner_radius(t::R_SM) - .stroke(egui::Stroke::new(1.0, t::BORDER_SUBTLE)) - .inner_margin(egui::Margin::symmetric(10, 6)) - .show(ui, |ui| { - ui.horizontal(|ui| { - let (icon, color) = action_icon(&entry.action); - ui.label( - RichText::new(icon) - .font(FontId::proportional(14.0)) - .color(color), - ); - ui.vertical(|ui| { - ui.horizontal(|ui| { - ui.label( - RichText::new(&entry.action) - .font(FontId::proportional(12.0)) - .color(t::TEXT_PRIMARY) - .strong(), - ); - ui.label( - RichText::new(format!("· by {}…", &entry.actor_id[..8.min(entry.actor_id.len())])) - .font(FontId::monospace(10.0)) - .color(t::TEXT_DISABLED), - ); - }); - if let Some(t_id) = &entry.target_id { - ui.label( - RichText::new(format!("target: {}…", &t_id[..8.min(t_id.len())])) - .font(FontId::monospace(10.0)) - .color(t::TEXT_MUTED), - ); - } - if let Some(reason) = &entry.reason { - ui.label( - RichText::new(format!("reason: {reason}")) - .font(FontId::proportional(11.0)) - .color(t::TEXT_MUTED) - .italics(), - ); - } - }); - ui.with_layout(egui::Layout::right_to_left(egui::Align::Center), |ui| { - ui.label( - RichText::new(fmt_audit_ts(entry.created_at)) - .font(FontId::monospace(10.0)) - .color(t::TEXT_DISABLED), - ); - }); - }); - }); - ui.add_space(2.0); - } - }); - }); -} - -fn action_icon(action: &str) -> (&'static str, egui::Color32) { - match action { - "guild_create" | "GUILD_CREATE" => ("[+]", t::SUCCESS), - "guild_delete" | "GUILD_DELETE" => ("[-]", t::ERROR), - "member_kick" | "MEMBER_KICK" => ("[kick]", t::ERROR), - "member_leave" | "MEMBER_LEAVE" => ("[leave]", t::WARNING), - "role_create" | "ROLE_CREATE" => ("[role+]", t::INFO), - "role_delete" | "ROLE_DELETE" => ("[role-]", t::ERROR), - "role_assign" | "ROLE_ASSIGN" => ("[role=]", t::INFO), - "role_unassign" | "ROLE_UNASSIGN" => ("[role!=]", t::WARNING), - "invite_create" | "INVITE_CREATE" => ("[invite+]", t::SUCCESS), - "invite_delete" | "INVITE_DELETE" => ("[invite-]", t::ERROR), - "invite_accept" | "INVITE_ACCEPT" => ("[invite=]", t::SUCCESS), - _ => ("[*]", t::TEXT_MUTED), - } -} - -fn fmt_audit_ts(ts: i64) -> String { - let now = std::time::SystemTime::now() - .duration_since(std::time::UNIX_EPOCH) - .map(|d| d.as_secs() as i64) - .unwrap_or(0); - let diff = (now - ts / 1000).max(0); - if diff < 60 { - format!("{diff}s ago") - } else if diff < 3600 { - format!("{}m ago", diff / 60) - } else if diff < 86400 { - format!("{}h ago", diff / 3600) - } else { - format!("{}d ago", diff / 86400) - } -} - -fn show_members_popup(ctx: &egui::Context, s: &mut UiState, net: &NetHandle) { - egui::Window::new("Guild Members") - .collapsible(false) - .resizable(true) - .default_size([520.0, 460.0]) - .min_size([400.0, 320.0]) - .anchor(egui::Align2::CENTER_CENTER, [0.0, 0.0]) - .frame( - Frame::window(&ctx.style()) - .fill(t::BG_ELEVATED) - .stroke(egui::Stroke::new(1.0, t::BORDER_DEFAULT)) - .corner_radius(t::R_LG), - ) - .show(ctx, |ui| { - ui.set_width(500.0); - ui.horizontal(|ui| { - let count = s.guild_member_list.len(); - ui.label( - RichText::new(format!("{count} members")) - .font(FontId::proportional(11.0)) - .color(t::TEXT_MUTED), - ); - ui.with_layout(egui::Layout::right_to_left(egui::Align::Center), |ui| { - if t::icon_btn_drawn(ui, vec_icons::refresh, "refresh", None, 18.0).clicked() - && let Some(gid) = s.guild_member_list_guild_id.clone() - { - let net = net.clone(); - tokio::spawn(async move { - net.send(NetCommand::GuildMemberListFetch { - guild_id: gid.clone(), - }) - .await; - net.send(NetCommand::GuildRoleListFetch { guild_id: gid }) - .await; - }); - } - if t::icon_btn_drawn(ui, vec_icons::close, "close", None, 18.0).clicked() { - s.guild_members_open = false; - } - }); - }); - ui.add_space(6.0); - ui.separator(); - ui.add_space(4.0); - - if s.guild_member_list.is_empty() { - ui.vertical_centered(|ui| { - ui.add_space(40.0); - ui.label( - RichText::new("no members") - .font(FontId::proportional(12.0)) - .color(t::TEXT_DISABLED), - ); - }); - return; - } - - // Determine if current user is owner (kick allowed). - // For simplicity, we allow kick UI for everyone; gateway enforces permission. - let roles_clone = s.guild_role_list.clone(); - let guild_id = s.guild_member_list_guild_id.clone().unwrap_or_default(); - - egui::ScrollArea::vertical() - .auto_shrink([false, true]) - .show(ui, |ui| { - for member in s.guild_member_list.clone() { - let role_color = - parse_hex_color(&member.role_color).unwrap_or(t::TEXT_PRIMARY); - Frame::NONE - .fill(t::BG_SURFACE) - .corner_radius(t::R_SM) - .stroke(egui::Stroke::new(1.0, t::BORDER_SUBTLE)) - .inner_margin(egui::Margin::symmetric(10, 6)) - .show(ui, |ui| { - ui.horizontal(|ui| { - // Avatar with role-colored ring - let init = member - .nickname - .chars() - .next() - .unwrap_or('?') - .to_uppercase() - .to_string(); - let (r, _) = ui.allocate_exact_size( - egui::vec2(28.0, 28.0), - egui::Sense::hover(), - ); - ui.painter().circle_filled(r.center(), 14.0, role_color); - ui.painter().text( - r.center(), - egui::Align2::CENTER_CENTER, - &init, - egui::FontId::proportional(12.0), - egui::Color32::from_rgb(15, 15, 15), - ); - - ui.add_space(8.0); - ui.vertical(|ui| { - ui.horizontal(|ui| { - ui.label( - RichText::new(&member.nickname) - .font(FontId::proportional(13.0)) - .color(t::TEXT_PRIMARY) - .strong(), - ); - if member.is_owner { - let (cr, _) = ui.allocate_exact_size( - egui::vec2(14.0, 14.0), - egui::Sense::hover(), - ); - vec_icons::crown(ui.painter(), cr, t::WARNING); - } - }); - ui.horizontal(|ui| { - let (dot_r, _) = ui.allocate_exact_size( - egui::vec2(8.0, 8.0), - egui::Sense::hover(), - ); - ui.painter().circle_filled( - dot_r.center(), - 4.0, - role_color, - ); - ui.label( - RichText::new(&member.role_name) - .font(FontId::proportional(10.0)) - .color(t::TEXT_MUTED), - ); - ui.label( - RichText::new(format!( - "· {}…", - &member.user_id[..8.min(member.user_id.len())] - )) - .font(FontId::monospace(10.0)) - .color(t::TEXT_DISABLED), - ); - }); - }); - - ui.with_layout( - egui::Layout::right_to_left(egui::Align::Center), - |ui| { - // Role-assign combo (only shows if user has roles defined). - if !roles_clone.is_empty() && !member.is_owner { - let combo_text = "+ role".to_string(); - egui::ComboBox::from_id_salt(format!( - "role_combo_{}", - member.user_id - )) - .selected_text(combo_text) - .width(120.0) - .show_ui(ui, |ui| { - for role in &roles_clone { - if ui - .add( - egui::Button::new( - RichText::new(&role.name) - .font(FontId::proportional( - 11.0, - )) - .color( - parse_hex_color( - &role.color, - ) - .unwrap_or( - t::TEXT_PRIMARY, - ), - ), - ) - .fill(egui::Color32::TRANSPARENT) - .frame(false), - ) - .clicked() - { - let net = net.clone(); - let gid = guild_id.clone(); - let uid = member.user_id.clone(); - let rid = role.id.clone(); - tokio::spawn(async move { - net.send( - NetCommand::GuildRoleAssign { - guild_id: gid, - user_id: uid, - role_id: rid, - }, - ) - .await; - }); - } - } - }); - } - // Kick button (hidden for owner). - if !member.is_owner - && t::icon_btn_drawn( - ui, - vec_icons::boot, - "kick (admins only)", - Some(t::ERROR), - 18.0, - ) - .clicked() - { - let net_kick = net.clone(); - let net_refresh = net.clone(); - let gid = guild_id.clone(); - let uid = member.user_id.clone(); - tokio::spawn(async move { - net_kick - .send(NetCommand::GuildMemberKick { - guild_id: gid.clone(), - user_id: uid, - }) - .await; - tokio::time::sleep( - std::time::Duration::from_millis(200), - ) - .await; - net_refresh - .send(NetCommand::GuildMemberListFetch { - guild_id: gid, - }) - .await; - }); - } - }, - ); - }); - }); - ui.add_space(2.0); - } - }); - }); -} - -fn parse_hex_color(hex: &str) -> Option { - let hex = hex.trim_start_matches('#'); - if hex.len() != 6 { - return None; - } - let r = u8::from_str_radix(&hex[0..2], 16).ok()?; - let g = u8::from_str_radix(&hex[2..4], 16).ok()?; - let b = u8::from_str_radix(&hex[4..6], 16).ok()?; - Some(egui::Color32::from_rgb(r, g, b)) -} diff --git a/src/ui/sidebar/mod.rs b/src/ui/sidebar/mod.rs deleted file mode 100644 index 0fd8900..0000000 --- a/src/ui/sidebar/mod.rs +++ /dev/null @@ -1,94 +0,0 @@ -//! Unified left sidebar (design from aiupdate). - -mod channel_list; -mod guild_bar; -mod sections; - -pub use guild_bar::show as guild_bar; - -use eframe::egui::{self, Context, Frame, SidePanel}; -use vnox_client::{identity::Identity, net::NetHandle}; - -use crate::{ - connect, - state::{ConnState, UiState}, - theme as t, -}; - -pub fn show(ctx: &Context, s: &mut UiState, identity: &Identity, net: &NetHandle) { - SidePanel::left("sidebar") - .exact_width(220.0) - .resizable(false) - .frame(Frame::NONE.fill(t::BG_SURFACE)) - .show_separator_line(false) - .show(ctx, |ui| { - let r = ui.max_rect(); - ui.painter().vline( - r.right(), - r.y_range(), - egui::Stroke::new(1.0, t::BORDER_SUBTLE), - ); - - ui.with_layout(egui::Layout::top_down_justified(egui::Align::LEFT), |ui| { - sections::header::show(ui, s); - bookmark_row(ui, s, net); - - let userbar_h = 52.0; - let avail = (ui.available_height() - userbar_h).max(0.0); - egui::ScrollArea::vertical() - .max_height(avail) - .auto_shrink([false; 2]) - .show(ui, |ui| { - ui.add_space(6.0); - channel_list::show(ui, s, net); - ui.add_space(4.0); - sections::dm::show(ui, s, net); - ui.add_space(6.0); - }); - - sections::userbar::show(ui, s, identity, net); - }); - }); -} - -fn bookmark_row(ui: &mut egui::Ui, s: &mut UiState, net: &NetHandle) { - let busy = matches!( - s.conn, - ConnState::Connecting | ConnState::Reconnecting { .. } - ); - if s.bookmarks.is_empty() { - return; - } - ui.horizontal_wrapped(|ui| { - ui.spacing_mut().item_spacing = egui::vec2(4.0, 4.0); - ui.add_space(8.0); - for (i, bm) in s.bookmarks.clone().into_iter().enumerate() { - let active = s.selected_bookmark == i; - let label = bm.label.chars().take(8).collect::(); - let btn = egui::Button::new( - egui::RichText::new(label) - .font(egui::FontId::proportional(10.0)) - .color(if active { - t::ACCENT_SOFT - } else { - t::TEXT_MUTED - }), - ) - .fill(if active { t::ACCENT_10 } else { t::BG_ELEVATED }) - .stroke(egui::Stroke::new( - 1.0, - if active { - t::BORDER_ACCENT - } else { - t::BORDER_DEFAULT - }, - )) - .corner_radius(t::R_SM); - if ui.add(btn).on_hover_text(&bm.address).clicked() && !busy { - s.selected_bookmark = i; - connect::connect_to(s, net, &bm.address); - } - } - }); - ui.add_space(4.0); -} diff --git a/src/ui/sidebar/sections/dm.rs b/src/ui/sidebar/sections/dm.rs deleted file mode 100644 index 608da42..0000000 --- a/src/ui/sidebar/sections/dm.rs +++ /dev/null @@ -1,170 +0,0 @@ -use eframe::egui::{self, Frame}; -use vnox_client::net::{NetCommand, NetHandle}; - -use crate::state::{ConnState, UiState}; -use crate::theme as t; - -pub fn show(ui: &mut egui::Ui, s: &mut UiState, net: &NetHandle) { - if !matches!(s.conn, ConnState::Connected { .. }) { - return; - } - Frame::NONE - .inner_margin(egui::Margin { - left: 12, - right: 8, - top: 2, - bottom: 2, - }) - .show(ui, |ui| { - ui.horizontal(|ui| { - ui.label( - egui::RichText::new("DIRECT MESSAGES") - .font(egui::FontId::proportional(10.0)) - .color(t::TEXT_MUTED), - ); - ui.with_layout(egui::Layout::right_to_left(egui::Align::Center), |ui| { - if ui - .add( - egui::Button::new( - egui::RichText::new("+") - .font(egui::FontId::proportional(12.0)) - .color(t::ACCENT), - ) - .frame(false) - .min_size(egui::vec2(16.0, 16.0)), - ) - .on_hover_text("new DM") - .clicked() - { - s.new_dm_input = Some(String::new()); - } - }); - }); - }); - new_dm_input(ui, s, net); - for conv in s.dm_conversations.clone() { - let active = s.active_dm_id.as_deref() == Some(&conv.dm_id); - let bg = if active { - t::ACCENT_10 - } else { - egui::Color32::TRANSPARENT - }; - let stroke = if active { - egui::Stroke::new(1.0, t::BORDER_ACCENT) - } else { - egui::Stroke::NONE - }; - let text_c = if active { - t::ACCENT_SOFT - } else { - t::TEXT_MUTED - }; - - let resp = Frame::NONE - .fill(bg) - .stroke(stroke) - .corner_radius(t::R_SM) - .inner_margin(egui::Margin::symmetric(8, 3)) - .show(ui, |ui| { - ui.horizontal(|ui| { - ui.label(egui::RichText::new("@").color(text_c).size(13.0)); - ui.label( - egui::RichText::new(&conv.other_nickname) - .color(text_c) - .size(13.0), - ); - ui.with_layout(egui::Layout::right_to_left(egui::Align::Center), |ui| { - if conv.unread_count > 0 { - let text = if conv.unread_count > 99 { - "99+".to_string() - } else { - conv.unread_count.to_string() - }; - let (r, _) = ui - .allocate_exact_size(egui::vec2(18.0, 14.0), egui::Sense::hover()); - ui.painter() - .rect_filled(r, egui::CornerRadius::same(4), t::ACCENT); - ui.painter().text( - r.center(), - egui::Align2::CENTER_CENTER, - &text, - egui::FontId::monospace(10.0), - egui::Color32::WHITE, - ); - } - }); - }); - }) - .response - .interact(egui::Sense::click()); - - if resp.clicked() { - let clicked_dm_id = conv.dm_id.clone(); - s.active_dm_id = Some(clicked_dm_id.clone()); - if let Some(c) = s - .dm_conversations - .iter_mut() - .find(|c| c.dm_id == conv.dm_id) - { - c.unread_count = 0; - } - let net = net.clone(); - tokio::spawn(async move { - net.send(NetCommand::DmReadAck { - dm_id: clicked_dm_id, - }) - .await; - }); - } - } -} - -pub fn new_dm_input(ui: &mut egui::Ui, s: &mut UiState, net: &NetHandle) { - let mut input_buf = s.new_dm_input.clone().unwrap_or_default(); - let mut should_start = false; - if s.new_dm_input.is_some() { - Frame::NONE - .inner_margin(egui::Margin::symmetric(12, 2)) - .show(ui, |ui| { - ui.horizontal(|ui| { - let resp = ui.add_sized( - egui::vec2(120.0, 20.0), - egui::TextEdit::singleline(&mut input_buf) - .font(egui::FontId::proportional(11.0)) - .text_color(t::TEXT_PRIMARY) - .hint_text(egui::RichText::new("user id").color(t::TEXT_DISABLED)) - .frame(true), - ); - if resp.lost_focus() && ui.input(|i| i.key_pressed(egui::Key::Enter)) - || ui - .add( - egui::Button::new( - egui::RichText::new("Start") - .font(egui::FontId::proportional(10.0)) - .color(t::ACCENT), - ) - .fill(t::ACCENT_10) - .stroke(egui::Stroke::new(1.0, t::BORDER_ACCENT)) - .corner_radius(t::R_SM) - .min_size(egui::vec2(40.0, 18.0)), - ) - .clicked() - && !input_buf.trim().is_empty() - { - should_start = true; - } - }); - }); - } - if should_start { - let target = input_buf.trim().to_string(); - let net = net.clone(); - tokio::spawn(async move { - net.send(NetCommand::DmStart { - target_user_id: target, - }) - .await; - }); - s.new_dm_input = None; - } -} diff --git a/src/ui/sidebar/sections/header.rs b/src/ui/sidebar/sections/header.rs deleted file mode 100644 index cbbeb65..0000000 --- a/src/ui/sidebar/sections/header.rs +++ /dev/null @@ -1,95 +0,0 @@ -use eframe::egui::{self, Frame}; - -use crate::state::{ConnState, UiState}; -use crate::theme as t; - -pub fn show(ui: &mut egui::Ui, s: &UiState) { - let title = if let Some(gid) = &s.active_guild_id - && let Some(g) = s.guilds.iter().find(|g| &g.guild_id == gid) - { - g.name.clone() - } else { - match &s.conn { - ConnState::Connected { node_name, .. } => node_name.clone(), - ConnState::Reconnecting { attempt } => format!("reconnect #{attempt}"), - ConnState::Connecting => "connecting…".into(), - ConnState::Disconnected => "not connected".into(), - } - }; - - let is_guild = s.active_guild_id.is_some(); - - Frame::NONE - .fill(t::BG_SURFACE) - .inner_margin(egui::Margin::symmetric(12, 0)) - .show(ui, |ui| { - ui.set_height(38.0); - ui.horizontal_centered(|ui| { - if is_guild { - let (r, _) = - ui.allocate_exact_size(egui::vec2(22.0, 22.0), egui::Sense::hover()); - ui.painter().rect_filled(r, t::R_FULL, t::ACCENT); - ui.painter().text( - r.center(), - egui::Align2::CENTER_CENTER, - title - .chars() - .next() - .unwrap_or('?') - .to_uppercase() - .to_string(), - egui::FontId::monospace(11.0), - egui::Color32::WHITE, - ); - } else { - let (r, _) = - ui.allocate_exact_size(egui::vec2(22.0, 22.0), egui::Sense::hover()); - ui.painter().rect_filled(r, t::R_SM, t::ACCENT_20); - ui.painter().rect_stroke( - r, - t::R_SM, - egui::Stroke::new(1.0, t::BORDER_ACCENT), - egui::StrokeKind::Inside, - ); - ui.painter().text( - r.center(), - egui::Align2::CENTER_CENTER, - "V", - egui::FontId::monospace(11.0), - t::TEXT_PRIMARY, - ); - } - - ui.add_space(8.0); - ui.label( - egui::RichText::new(title) - .font(egui::FontId::proportional(13.0)) - .color(t::TEXT_PRIMARY) - .strong(), - ); - - if s.private_mode { - ui.add_space(6.0); - Frame::NONE - .fill(t::WARNING) - .corner_radius(t::R_SM) - .inner_margin(egui::Margin::symmetric(4, 1)) - .show(ui, |ui| { - ui.label( - egui::RichText::new("PRIVATE") - .font(egui::FontId::monospace(8.0)) - .color(egui::Color32::from_rgb(15, 15, 15)) - .strong(), - ); - }); - } - }); - }); - - let sep = ui.max_rect(); - ui.painter().hline( - sep.x_range(), - sep.top() + 38.0, - egui::Stroke::new(1.0, t::BORDER_SUBTLE), - ); -} diff --git a/src/ui/sidebar/sections/mod.rs b/src/ui/sidebar/sections/mod.rs deleted file mode 100644 index 44064bd..0000000 --- a/src/ui/sidebar/sections/mod.rs +++ /dev/null @@ -1,3 +0,0 @@ -pub mod dm; -pub mod header; -pub mod userbar; diff --git a/src/ui/sidebar/sections/userbar.rs b/src/ui/sidebar/sections/userbar.rs deleted file mode 100644 index 9d5e3ae..0000000 --- a/src/ui/sidebar/sections/userbar.rs +++ /dev/null @@ -1,281 +0,0 @@ -use eframe::egui::{self, FontId, Frame, RichText}; -use vnox_client::{ - identity::Identity, - net::{NetCommand, NetHandle}, -}; - -use crate::state::UiState; -use crate::theme as t; -use crate::theme::icons; - -pub fn show(ui: &mut egui::Ui, s: &mut UiState, identity: &Identity, net: &NetHandle) { - let r = ui.available_rect_before_wrap(); - ui.painter().hline( - r.x_range(), - r.top(), - egui::Stroke::new(1.0, t::BORDER_SUBTLE), - ); - - Frame::NONE - .fill(t::BG_ELEVATED) - .inner_margin(egui::Margin::symmetric(10, 8)) - .show(ui, |ui| { - ui.horizontal(|ui| { - let init = identity - .nickname - .chars() - .next() - .unwrap_or('U') - .to_uppercase() - .to_string(); - t::avatar( - ui, - &init, - avatar_color_for(&identity.nickname, s.local_speaking), - 30.0, - ); - ui.add_space(8.0); - - ui.vertical(|ui| { - ui.spacing_mut().item_spacing.y = 1.0; - ui.horizontal(|ui| { - ui.label( - egui::RichText::new(&identity.nickname) - .font(egui::FontId::proportional(12.5)) - .color(if s.local_speaking { - t::SUCCESS - } else { - t::TEXT_PRIMARY - }) - .strong(), - ); - if s.local_speaking { - let (r, _) = ui - .allocate_exact_size(egui::vec2(10.0, 10.0), egui::Sense::hover()); - icons::speaker(ui.painter(), r, t::SUCCESS); - } - }); - - // Status text — shows voice state, custom status, or activity. - let status_text = if let Some(ch) = s.active_voice_channel() { - if s.remote_speaking { - format!("in voice · {} · talking", ch.name) - } else { - format!("in voice · {}", ch.name) - } - } else if let Some(ref custom) = s.custom_status { - custom.clone() - } else if let (Some(_kind), Some(text)) = (&s.activity_type, &s.activity_text) { - text.clone() - } else if s.overlay_latency { - match s.rtt_ms { - Some(ms) => format!("online · {ms}ms"), - None => "online".into(), - } - } else { - "online".into() - }; - - let status_label = ui.add( - egui::Button::new( - RichText::new(status_text) - .font(FontId::proportional(10.0)) - .color(t::TEXT_MUTED), - ) - .fill(egui::Color32::TRANSPARENT) - .frame(false), - ); - status_label.context_menu(|ui| { - status_picker(ui, s, net); - ui.separator(); - custom_status_editor(ui, s, net); - ui.separator(); - activity_editor(ui, s, net); - }); - }); - - ui.with_layout(egui::Layout::right_to_left(egui::Align::Center), |ui| { - ui.spacing_mut().item_spacing.x = 1.0; - if t::icon_btn_drawn(ui, icons::gear, "settings", None, 22.0).clicked() { - s.settings_open = true; - } - let deaf_c = if s.deafen { Some(t::ERROR) } else { None }; - let deaf_icon = if s.deafen { - icons::headphones_off - } else { - icons::headphones - }; - if t::icon_btn_drawn(ui, deaf_icon, "deafen", deaf_c, 22.0).clicked() { - s.deafen = !s.deafen; - } - let mic_icon = if s.mic_enabled { - icons::microphone - } else { - icons::microphone_off - }; - let mic_c = if !s.mic_enabled { - Some(t::ERROR) - } else if s.local_speaking { - Some(t::SUCCESS) - } else { - None - }; - if t::icon_btn_drawn(ui, mic_icon, "microphone", mic_c, 22.0).clicked() { - s.mic_enabled = !s.mic_enabled; - } - }); - }); - }); -} - -fn status_picker(ui: &mut egui::Ui, s: &mut UiState, net: &NetHandle) { - ui.label( - RichText::new("Status") - .font(FontId::proportional(10.0)) - .color(t::TEXT_MUTED) - .strong(), - ); - let options = [ - ("online", "Online"), - ("idle", "Idle"), - ("dnd", "Do Not Disturb"), - ("invisible", "Invisible"), - ]; - for (key, label) in options { - let selected = s.presence_status.as_deref() == Some(key); - let btn = egui::Button::new(RichText::new(label).font(FontId::proportional(12.0))) - .fill(if selected { - t::ACCENT_10 - } else { - egui::Color32::TRANSPARENT - }) - .frame(false); - if ui.add(btn).clicked() { - s.presence_status = Some(key.to_string()); - send_presence(s, net); - ui.close(); - } - } -} - -fn custom_status_editor(ui: &mut egui::Ui, s: &mut UiState, net: &NetHandle) { - ui.label( - RichText::new("Custom status") - .font(FontId::proportional(10.0)) - .color(t::TEXT_MUTED) - .strong(), - ); - let mut buf = s.custom_status.clone().unwrap_or_default(); - let resp = ui.add( - egui::TextEdit::singleline(&mut buf) - .font(FontId::proportional(12.0)) - .hint_text("what's on your mind?") - .desired_width(180.0) - .text_color(t::TEXT_PRIMARY), - ); - if resp.changed() { - s.custom_status = if buf.trim().is_empty() { - None - } else { - Some(buf) - }; - } - if resp.lost_focus() { - send_presence(s, net); - } - if ui - .add( - egui::Button::new( - RichText::new("Clear") - .font(FontId::proportional(11.0)) - .color(t::TEXT_MUTED), - ) - .fill(egui::Color32::TRANSPARENT) - .frame(false), - ) - .clicked() - { - s.custom_status = None; - send_presence(s, net); - ui.close(); - } -} - -fn activity_editor(ui: &mut egui::Ui, s: &mut UiState, net: &NetHandle) { - ui.label( - RichText::new("Activity") - .font(FontId::proportional(10.0)) - .color(t::TEXT_MUTED) - .strong(), - ); - let kinds = [ - ("playing", "Playing"), - ("listening", "Listening"), - ("watching", "Watching"), - ("streaming", "Streaming"), - ]; - let mut current_kind = s.activity_type.clone(); - let combo = egui::ComboBox::from_id_salt("activity_kind") - .selected_text(current_kind.clone().unwrap_or_else(|| "— none —".into())) - .width(160.0); - combo.show_ui(ui, |ui| { - for (k, label) in kinds { - ui.selectable_value(&mut current_kind, Some(k.to_string()), label); - } - ui.selectable_value(&mut current_kind, None, "— none —"); - }); - let mut buf = s.activity_text.clone().unwrap_or_default(); - let resp = ui.add( - egui::TextEdit::singleline(&mut buf) - .font(FontId::proportional(12.0)) - .hint_text("doing what?") - .desired_width(180.0) - .text_color(t::TEXT_PRIMARY), - ); - let mut changed = false; - if resp.changed() { - s.activity_text = if buf.trim().is_empty() { - None - } else { - Some(buf) - }; - changed = true; - } - if current_kind != s.activity_type { - s.activity_type = current_kind; - changed = true; - } - if changed && (resp.lost_focus() || ui.input(|i| i.key_pressed(egui::Key::Enter))) { - send_presence(s, net); - } -} - -fn send_presence(s: &UiState, net: &NetHandle) { - let net = net.clone(); - let status = s.presence_status.clone().unwrap_or_else(|| "online".into()); - let custom = s.custom_status.clone(); - let activity = s.activity_type.clone(); - let activity_text = s.activity_text.clone(); - tokio::spawn(async move { - net.send(NetCommand::PresenceUpdate { - status, - custom_status: custom, - activity, - activity_text, - }) - .await; - }); -} - -fn avatar_color_for(nick: &str, speaking: bool) -> egui::Color32 { - if speaking { - return t::SUCCESS; - } - let colors = [ - t::ACCENT_10, - egui::Color32::from_rgb(50, 70, 100), - egui::Color32::from_rgb(60, 80, 50), - egui::Color32::from_rgb(80, 60, 80), - ]; - colors[nick.bytes().fold(0usize, |a, b| a.wrapping_add(b as usize)) % colors.len()] -} diff --git a/src/ui/slint/chat.slint b/src/ui/slint/chat.slint new file mode 100644 index 0000000..02ac3a6 --- /dev/null +++ b/src/ui/slint/chat.slint @@ -0,0 +1,123 @@ +import { Theme } from "theme.slint"; + +export struct MessageItem { + message_id: string, + sender_id: string, + sender_name: string, + content: string, + timestamp: string, + is_own: bool, + edited: bool, + reply_to: string, +} + +export struct DmConversationItem { + dm_id: string, + other_user_id: string, + other_nickname: string, + e2ee_enabled: bool, + messages: [MessageItem], +} + +export struct MemberItem { + user_id: string, + nickname: string, + status: string, +} + +export component ChatPanel { + in-out property active-channel-id; + in-out property active-channel-name; + in-out property <[MessageItem]> messages; + in-out property <[MemberItem]> members; + in-out property input-text; + in-out property is-dm; + in-out property e2ee-enabled; + in-out property dm-partner-name; + + callback send-message(string, string); + callback load-more(string); + + VerticalLayout { + padding: 0; + spacing: 0; + + Rectangle { + height: 48px; + background: Theme.bg-surface; + HorizontalLayout { + padding: 8px; + spacing: 4px; + Text { + text: e2ee-enabled ? "🔒 " : ""; + color: Theme.accent; + } + Text { + text: is-dm ? dm-partner-name : "#" + active-channel-name; + color: Theme.text-primary; + font-size: 16px; + } + } + } + + Flickable { + VerticalLayout { + for msg in messages: Rectangle { + height: 60px; + VerticalLayout { + padding: 8px; + spacing: 4px; + HorizontalLayout { + spacing: 8px; + Text { + text: msg.sender_name; + color: Theme.accent; + font-weight: 700; + } + Text { + text: msg.timestamp; + color: Theme.text-muted; + font-size: 11px; + } + } + Text { + text: msg.content; + wrap: word-wrap; + } + } + } + } + } + + Rectangle { + height: 48px; + background: Theme.bg-surface; + HorizontalLayout { + padding: 8px; + spacing: 8px; + TextInput { + text <=> input-text; + horizontal-stretch: 1; + } + Rectangle { + width: 64px; + height: 32px; + background: Theme.accent; + border-radius: Theme.radius; + TouchArea { + clicked => { + send-message(active-channel-id, input-text); + input-text = ""; + } + } + Text { + text: "Send"; + color: Theme.text-primary; + horizontal-alignment: center; + vertical-alignment: center; + } + } + } + } + } +} diff --git a/src/ui/slint/connect.slint b/src/ui/slint/connect.slint new file mode 100644 index 0000000..acea231 --- /dev/null +++ b/src/ui/slint/connect.slint @@ -0,0 +1,101 @@ +import { Theme } from "theme.slint"; +import { Button, ListView, LineEdit } from "std-widgets.slint"; + +export struct BookmarkItem { + name: string, + address: string, +} + +export component ConnectScreen { + in-out property <[BookmarkItem]> bookmarks; + in-out property address-input; + in-out property status-text; + in-out property connecting; + in-out property auto-reconnect; + in-out property node-name; + + callback connect(string); + callback disconnect(); + callback remove-bookmark(string); + callback add-bookmark(string); + + VerticalLayout { + alignment: center; + spacing: 16px; + + Text { text: "VNOX"; color: Theme.accent; font-size: 36px; font-weight: 700; } + Text { text: "Self-hosted voice & chat"; color: Theme.text-secondary; font-size: 14px; } + + HorizontalLayout { + LineEdit { + text <=> address-input; + placeholder-text: "server:7600"; + width: 300px; + } + Rectangle { + width: 100px; + height: 30px; + background: Theme.accent; + border-radius: 6px; + TouchArea { + enabled: !connecting; + clicked => { connect(address-input); } + } + Text { + text: connecting ? "Connecting..." : "Connect"; + color: Theme.text-primary; + horizontal-alignment: center; + vertical-alignment: center; + } + } + } + + Text { text: status-text; color: Theme.text-muted; } + + HorizontalLayout { + Rectangle { + width: 16px; + height: 16px; + background: auto-reconnect ? Theme.accent : Theme.bg-elevated; + border-radius: 3px; + border-width: 1px; + border-color: Theme.text-muted; + TouchArea { + clicked => { auto-reconnect = !auto-reconnect; } + } + } + Text { text: "Auto-reconnect"; color: Theme.text-secondary; } + } + + Text { text: "Saved Servers"; color: Theme.text-primary; font-size: 16px; } + ListView { + for bm in bookmarks: Rectangle { + height: 40px; + HorizontalLayout { + padding: 8px; + Text { text: bm.name; color: Theme.text-primary; } + Text { text: bm.address; color: Theme.text-muted; } + HorizontalLayout { + Button { text: "Connect"; clicked => { connect(bm.address); } } + Button { text: "✕"; clicked => { remove-bookmark(bm.address); } } + } + } + } + } + + HorizontalLayout { + Button { text: "+ Add Server"; clicked => { add-bookmark(address-input); } } + } + + Text { text: node-name; color: Theme.success; } + Rectangle { + width: 100px; + height: 30px; + background: Theme.danger; + border-radius: 6px; + visible: node-name != ""; + TouchArea { clicked => { disconnect(); } } + Text { text: "Disconnect"; color: Theme.text-primary; horizontal-alignment: center; vertical-alignment: center; } + } + } +} diff --git a/src/ui/slint/friends.slint b/src/ui/slint/friends.slint new file mode 100644 index 0000000..ddb8ce7 --- /dev/null +++ b/src/ui/slint/friends.slint @@ -0,0 +1,123 @@ +import { Theme } from "theme.slint"; + +export struct FriendItem { + user_id: string, + nickname: string, + status: string, +} + +export component FriendsPanel { + in-out property <[FriendItem]> friends; + in-out property <[FriendItem]> pending-requests; + in-out property <[string]> blocked-users; + in-out property active-tab; + in-out property add-friend-input; + + callback add-friend(string); + callback accept-friend(string); + callback decline-friend(string); + callback remove-friend(string); + callback start-dm(string); + + VerticalLayout { + padding: 16px; + spacing: 8px; + + Text { + text: "Friends"; + color: Theme.text-primary; + font-size: 18px; + } + + HorizontalLayout { + spacing: 4px; + Rectangle { + height: 28px; + background: Theme.bg-interactive; + border-radius: Theme.radius; + TouchArea { + clicked => { active-tab = "online"; } + } + Text { text: "Online"; color: Theme.text-primary; horizontal-alignment: center; vertical-alignment: center; } + } + Rectangle { + height: 28px; + background: Theme.bg-interactive; + border-radius: Theme.radius; + TouchArea { + clicked => { active-tab = "all"; } + } + Text { text: "All"; color: Theme.text-primary; horizontal-alignment: center; vertical-alignment: center; } + } + Rectangle { + height: 28px; + background: Theme.bg-interactive; + border-radius: Theme.radius; + TouchArea { + clicked => { active-tab = "pending"; } + } + Text { text: "Pending"; color: Theme.text-primary; horizontal-alignment: center; vertical-alignment: center; } + } + Rectangle { + height: 28px; + background: Theme.bg-interactive; + border-radius: Theme.radius; + TouchArea { + clicked => { active-tab = "blocked"; } + } + Text { text: "Blocked"; color: Theme.text-primary; horizontal-alignment: center; vertical-alignment: center; } + } + } + + HorizontalLayout { + spacing: 8px; + TextInput { + text <=> add-friend-input; + horizontal-stretch: 1; + } + Rectangle { + width: 32px; + height: 32px; + background: Theme.accent; + border-radius: Theme.radius; + TouchArea { + clicked => { + add-friend(add-friend-input); + add-friend-input = ""; + } + } + Text { text: "+"; color: Theme.text-primary; horizontal-alignment: center; vertical-alignment: center; } + } + } + + Flickable { + VerticalLayout { + for f in friends: Rectangle { + height: 48px; + HorizontalLayout { + padding: 8px; + spacing: 8px; + Text { + text: f.nickname; + color: Theme.text-primary; + } + Text { + text: f.status; + color: Theme.text-muted; + } + Rectangle { + width: 40px; + height: 28px; + background: Theme.accent; + border-radius: Theme.radius; + TouchArea { + clicked => { start-dm(f.user_id); } + } + Text { text: "DM"; color: Theme.text-primary; horizontal-alignment: center; vertical-alignment: center; } + } + } + } + } + } + } +} diff --git a/src/ui/slint/main.slint b/src/ui/slint/main.slint new file mode 100644 index 0000000..05688c7 --- /dev/null +++ b/src/ui/slint/main.slint @@ -0,0 +1,147 @@ +import { Theme } from "theme.slint"; +import { Sidebar, ChannelItem, GuildItem } from "sidebar.slint"; +import { ChatPanel, MessageItem, MemberItem } from "chat.slint"; +import { FriendsPanel, FriendItem } from "friends.slint"; +import { VoicePanel, VoiceMemberItem } from "voice.slint"; +import { BookmarkItem, ConnectScreen } from "connect.slint"; + +export component MainWindow inherits Window { + in-out property <[ChannelItem]> channels; + in-out property <[GuildItem]> guilds; + in-out property <[string]> dm-conversations; + in-out property username; + in-out property user-status; + in-out property active-channel-id; + in-out property active-guild-idx; + + in-out property active-channel-name; + in-out property <[MessageItem]> messages; + in-out property <[MemberItem]> members; + in-out property input-text; + in-out property is-dm; + in-out property e2ee-enabled; + in-out property dm-partner-name; + in-out property show-chat; + in-out property show-friends; + in-out property show-voice; + + in-out property <[FriendItem]> friends; + in-out property <[FriendItem]> pending-requests; + in-out property <[string]> blocked-users; + in-out property friends-active-tab; + in-out property add-friend-input; + + in-out property <[VoiceMemberItem]> voice-members; + in-out property mic-enabled; + in-out property deafened; + in-out property voice-channel-name; + in-out property activity-banner; + + in-out property <[BookmarkItem]> bookmarks: []; + in-out property address-input: ""; + in-out property status-text: ""; + in-out property connecting: false; + in-out property auto-reconnect: false; + in-out property node-name: ""; + + in-out property identity-pubkey: ""; + in-out property vault-enabled: false; + in-out property voice-input-device: ""; + in-out property voice-output-device: ""; + in-out property voice-bitrate: 64.0; + in-out property vad-mode: 0; + in-out property volume: 100.0; + in-out property active-tab: "audio"; + + callback channel-selected(string); + callback guild-selected(int); + callback dm-selected(string); + callback disconnect(); + callback send-message(string, string); + callback load-more(string); + callback add-friend(string); + callback accept-friend(string); + callback decline-friend(string); + callback remove-friend(string); + callback start-dm(string); + callback toggle-mic(); + callback toggle-deafen(); + callback leave-voice(); + callback connect(string); + callback remove-bookmark(string); + callback add-bookmark(string); + callback close-settings(); + callback set-passphrase(string); + callback export-keyfile(); + callback import-keyfile(); + + min-width: 900px; + min-height: 560px; + title: "Vnox"; + background: Theme.bg-base; + + if node-name == "" : ConnectScreen { + bookmarks: root.bookmarks; + address-input: root.address-input; + status-text: root.status-text; + connecting: root.connecting; + auto-reconnect: root.auto-reconnect; + node-name: root.node-name; + connect(address) => { root.connect(address); } + disconnect => { root.disconnect(); } + remove-bookmark(address) => { root.remove-bookmark(address); } + add-bookmark(address) => { root.add-bookmark(address); } + } + if node-name != "" : HorizontalLayout { + sidebar := Sidebar { + channels: root.channels; + guilds: root.guilds; + dm-conversations: root.dm-conversations; + username: root.username; + user-status: root.user-status; + active-channel-id: root.active-channel-id; + active-guild-idx: root.active-guild-idx; + channel-selected(id) => { root.channel-selected(id); } + guild-selected(idx) => { root.guild-selected(idx); } + dm-selected(id) => { root.dm-selected(id); } + disconnect => { root.disconnect(); } + } + chat-panel := ChatPanel { + visible: show-chat; + active-channel-id: root.active-channel-id; + active-channel-name: root.active-channel-name; + messages: root.messages; + members: root.members; + input-text: root.input-text; + is-dm: root.is-dm; + e2ee-enabled: root.e2ee-enabled; + dm-partner-name: root.dm-partner-name; + send-message(id, text) => { root.send-message(id, text); } + load-more(id) => { root.load-more(id); } + } + friends-panel := FriendsPanel { + visible: show-friends; + friends: root.friends; + pending-requests: root.pending-requests; + blocked-users: root.blocked-users; + active-tab: root.friends-active-tab; + add-friend-input: root.add-friend-input; + add-friend(id) => { root.add-friend(id); } + accept-friend(id) => { root.accept-friend(id); } + decline-friend(id) => { root.decline-friend(id); } + remove-friend(id) => { root.remove-friend(id); } + start-dm(id) => { root.start-dm(id); } + } + voice-panel := VoicePanel { + visible: show-voice; + members: root.voice-members; + mic-enabled: root.mic-enabled; + deafened: root.deafened; + channel-name: root.voice-channel-name; + activity-banner: root.activity-banner; + toggle-mic => { root.toggle-mic(); } + toggle-deafen => { root.toggle-deafen(); } + leave-voice => { root.leave-voice(); } + } + } +} diff --git a/src/ui/slint/mod.rs b/src/ui/slint/mod.rs new file mode 100644 index 0000000..0cfe12a --- /dev/null +++ b/src/ui/slint/mod.rs @@ -0,0 +1 @@ +::slint::include_modules!(); diff --git a/src/ui/slint/settings.slint b/src/ui/slint/settings.slint new file mode 100644 index 0000000..c92b4fb --- /dev/null +++ b/src/ui/slint/settings.slint @@ -0,0 +1,68 @@ +import { Theme } from "theme.slint"; +import { Button } from "std-widgets.slint"; + +export component SettingsWindow inherits Window { + title: "Settings"; + width: 720px; + height: 520px; + background: Theme.bg-base; + + in-out property identity-pubkey; + in-out property vault-enabled; + in-out property voice-input-device; + in-out property voice-output-device; + in-out property voice-bitrate; + in-out property vad-mode; + in-out property volume; + in-out property active-tab; + + callback close(); + callback set-passphrase(string); + callback export-keyfile(); + callback import-keyfile(); + + VerticalLayout { + HorizontalLayout { + Button { text: "Audio"; clicked => { active-tab = "audio"; } } + Button { text: "Network"; clicked => { active-tab = "network"; } } + Button { text: "Identity"; clicked => { active-tab = "identity"; } } + Button { text: "App"; clicked => { active-tab = "app"; } } + } + + if active-tab == "identity": VerticalLayout { + Text { text: "Identity"; color: Theme.text-primary; font-size: 18px; } + Text { text: "Public Key: " + identity-pubkey; color: Theme.text-secondary; } + Button { text: vault-enabled ? "Remove Passphrase" : "Set Passphrase"; clicked => { set-passphrase(""); } } + Button { text: "Export Keyfile"; clicked => { export-keyfile(); } } + Button { text: "Import Keyfile"; clicked => { import-keyfile(); } } + } + + if active-tab == "audio": VerticalLayout { + Text { text: "Voice Settings"; color: Theme.text-primary; font-size: 18px; } + Text { text: "Input: " + voice-input-device; color: Theme.text-secondary; } + Text { text: "Output: " + voice-output-device; color: Theme.text-secondary; } + HorizontalLayout { + Text { text: "Bitrate:"; color: Theme.text-secondary; } + Text { text: voice-bitrate; color: Theme.text-primary; } + } + HorizontalLayout { + Text { text: "VAD Mode:"; color: Theme.text-secondary; } + Text { text: vad-mode == 0 ? "PTT" : vad-mode == 1 ? "VAD" : "Always On"; color: Theme.text-primary; } + } + HorizontalLayout { + Text { text: "Volume:"; color: Theme.text-secondary; } + Text { text: volume; color: Theme.text-primary; } + } + } + + if active-tab == "network": VerticalLayout { + Text { text: "Network"; color: Theme.text-primary; font-size: 18px; } + } + + if active-tab == "app": VerticalLayout { + Text { text: "Appearance"; color: Theme.text-primary; font-size: 18px; } + } + + Button { text: "Close"; horizontal-alignment: center; clicked => { close(); } } + } +} diff --git a/src/ui/slint/sidebar.slint b/src/ui/slint/sidebar.slint new file mode 100644 index 0000000..a282529 --- /dev/null +++ b/src/ui/slint/sidebar.slint @@ -0,0 +1,92 @@ +import { Theme } from "theme.slint"; + +export struct ChannelItem { + channel_id: string, + name: string, + kind: string, +} + +export struct GuildItem { + guild_id: string, + name: string, +} + +export component Sidebar { + in-out property <[GuildItem]> guilds; + in-out property <[ChannelItem]> channels; + in-out property <[string]> dm-conversations; + in-out property username; + in-out property user-status; + in-out property active-channel-id; + in-out property active-guild-idx; + + callback channel-selected(string); + callback guild-selected(int); + callback dm-selected(string); + callback disconnect(); + + width: 240px; + height: 100%; + + Rectangle { + background: Theme.bg-base; + width: 100%; + height: 100%; + + VerticalLayout { + Rectangle { + height: 48px; + background: Theme.bg-surface; + HorizontalLayout { + padding: 8px; + Text { text: "Vnox"; color: Theme.accent; font-size: 18px; } + } + } + + VerticalLayout { + for item in channels: Rectangle { + height: 36px; + background: item.channel_id == active-channel-id ? Theme.bg-elevated : Theme.bg-base; + HorizontalLayout { + padding: 8px; + Text { + text: item.kind == "voice" ? "🔊 " : "# "; + color: Theme.text-secondary; + } + Text { text: item.name; color: Theme.text-primary; } + } + TouchArea { + clicked => { channel-selected(item.channel_id); } + } + } + } + + VerticalLayout { + for dm in dm-conversations: Rectangle { + height: 36px; + HorizontalLayout { + padding: 8px; + Text { text: "@"; color: Theme.text-secondary; } + Text { text: dm; color: Theme.text-primary; } + } + TouchArea { + clicked => { dm-selected(dm); } + } + } + } + + Rectangle { + height: 52px; + background: Theme.bg-surface; + HorizontalLayout { + padding: 8px; + Text { text: username; color: Theme.text-primary; } + Text { text: user-status; color: Theme.text-muted; } + TouchArea { + clicked => { disconnect(); } + } + } + } + } + } +} diff --git a/src/ui/slint/theme.slint b/src/ui/slint/theme.slint new file mode 100644 index 0000000..461aa5b --- /dev/null +++ b/src/ui/slint/theme.slint @@ -0,0 +1,15 @@ +export global Theme { + in-out property bg-base: #181825; + in-out property bg-surface: #1e1e2e; + in-out property bg-hover: #2a2a3e; + in-out property bg-elevated: #2a2a3e; + in-out property bg-interactive: #313244; + in-out property accent: #E67E22; + in-out property text-primary: #cdd6f4; + in-out property text-secondary: #a0a4b8; + in-out property text-muted: #6c7086; + in-out property text-disabled: #45475a; + in-out property danger: #f38ba8; + in-out property success: #a6e3a1; + in-out property radius: 8px; +} diff --git a/src/ui/slint/voice.slint b/src/ui/slint/voice.slint new file mode 100644 index 0000000..3e2d983 --- /dev/null +++ b/src/ui/slint/voice.slint @@ -0,0 +1,99 @@ +import { Theme } from "theme.slint"; + +export struct VoiceMemberItem { + user_id: string, + nickname: string, + speaking: bool, +} + +export component VoicePanel { + in-out property <[VoiceMemberItem]> members; + in-out property mic-enabled; + in-out property deafened; + in-out property channel-name; + in-out property activity-banner; + + callback toggle-mic(); + callback toggle-deafen(); + callback leave-voice(); + + VerticalLayout { + padding: 16px; + spacing: 8px; + + Text { + text: "🔊 " + channel-name; + color: Theme.text-primary; + } + Text { + text: activity-banner; + color: Theme.text-muted; + } + Flickable { + VerticalLayout { + for m in members: Rectangle { + height: 48px; + HorizontalLayout { + padding: 8px; + spacing: 8px; + Rectangle { + width: 12px; + height: 12px; + border-radius: 6px; + background: m.speaking ? Theme.success : Theme.text-muted; + } + Text { + text: m.nickname; + color: Theme.text-primary; + } + } + } + } + } + HorizontalLayout { + spacing: 8px; + Rectangle { + height: 32px; + background: Theme.accent; + border-radius: Theme.radius; + TouchArea { + clicked => { toggle-mic(); } + } + Text { + text: mic-enabled ? "Mute" : "Unmute"; + color: Theme.text-primary; + horizontal-alignment: center; + vertical-alignment: center; + } + } + Rectangle { + height: 32px; + background: Theme.accent; + border-radius: Theme.radius; + TouchArea { + clicked => { toggle-deafen(); } + } + Text { + text: deafened ? "Undeafen" : "Deafen"; + color: Theme.text-primary; + horizontal-alignment: center; + vertical-alignment: center; + } + } + Rectangle { + height: 32px; + background: Theme.danger; + border-radius: Theme.radius; + TouchArea { + clicked => { leave-voice(); } + } + Text { + text: "Leave"; + color: Theme.text-primary; + horizontal-alignment: center; + vertical-alignment: center; + } + } + } + } +} diff --git a/src/ui/state/default.rs b/src/ui/state/default.rs index 7c28ced..8c44ddc 100644 --- a/src/ui/state/default.rs +++ b/src/ui/state/default.rs @@ -1,7 +1,6 @@ -use std::collections::{HashMap, HashSet}; -use std::time::Instant; +use std::collections::HashMap; -use super::models::{default_bookmarks, now_utc_hms}; +use super::models::default_bookmarks; use super::types::{ConnState, UiState, VoiceUiState}; impl Default for UiState { @@ -16,131 +15,42 @@ impl Default for UiState { active_channel: None, messages: Default::default(), chat_input: String::new(), - editing_message_id: None, - replying_to: None, - context_menu_message_id: None, scroll_bottom: false, voice: VoiceUiState::Off, voice_joined_at: None, user_names: Default::default(), - server_channel: None, rtt_ms: None, settings_open: false, - settings_page: Default::default(), mic_enabled: true, deafen: false, - noise_suppress: true, - echo_cancel: true, vad_mode: 0, - vad_threshold: 40.0, voice_bitrate: 64.0, packet_interval: 1, mic_device: 0, - input_volume: 80.0, - input_device_labels: vec!["Default".into()], - input_device_metas: Vec::new(), - output_device_labels: vec!["Default".into()], - output_device_metas: Vec::new(), - last_device_scan: None, output_device: 0, output_volume: 90.0, - jitter_size: 1, - adaptive_buffer: true, - overlay_enabled: false, - overlay_speakers: true, - overlay_latency: true, - - relay_enabled: true, - relay_address: "relay.nightcore.lnex".into(), - auto_relay: true, - force_relay: false, - udp_port: "7700".into(), auto_reconnect: true, - ui_scale: 100.0, - - kb_ptt: "mouse4".into(), - kb_mute: "ctrl+m".into(), - kb_deafen: "ctrl+d".into(), - kb_overlay: "ctrl+shift+o".into(), - - log_level: 0, - log_voice_packets: false, - show_packet_stats: false, - disable_encryption: false, - guilds: Vec::new(), active_guild_id: None, presences: Default::default(), - presence_status: Some("online".into()), - custom_status: None, - activity_type: None, - activity_text: None, friends: Vec::new(), pending_friend_requests: Vec::new(), blocked_users: Vec::new(), - block_input: None, - block_list_fetched_at: None, dm_conversations: Vec::new(), dm_messages: Default::default(), active_dm_id: None, new_dm_input: None, - add_server_open: false, - add_server_input: String::new(), - invite_accept_open: false, - invite_accept_code: String::new(), - create_guild_open: false, - create_guild_input: String::new(), - - create_channel_open: false, - create_channel_input: String::new(), - create_channel_kind: "text".into(), - - vault_set_open: false, - vault_remove_open: false, - vault_passphrase_input: String::new(), - vault_passphrase_confirm: String::new(), - vault_error: None, - - export_keyfile_open: false, - export_keyfile_pass: String::new(), - export_keyfile_confirm: String::new(), - export_keyfile_output: String::new(), - export_keyfile_error: None, - import_keyfile_open: false, - import_keyfile_input: String::new(), - import_keyfile_pass: String::new(), - import_keyfile_error: None, - - audit_log_open: false, - audit_log_guild_id: None, - audit_log_entries: Vec::new(), - - guild_members_open: false, - guild_member_list_guild_id: None, - guild_member_list: Vec::new(), - guild_role_list_guild_id: None, - guild_role_list: Vec::new(), - - add_friend_open: false, - add_friend_input: String::new(), - - dm_search_query: String::new(), user_colors: HashMap::new(), - per_user_volumes: Default::default(), - kb_listening: None, private_mode: false, - collapsed_categories: HashSet::new(), - startup_time_str: now_utc_hms(), typing_users: HashMap::new(), - last_typing_send: Instant::now(), read_receipts: HashMap::new(), friends_tab: "all".into(), local_speaking: false, diff --git a/src/ui/state/mod.rs b/src/ui/state/mod.rs index 9eedaf6..2dd6eba 100644 --- a/src/ui/state/mod.rs +++ b/src/ui/state/mod.rs @@ -2,57 +2,12 @@ mod default; mod models; mod types; -pub use models::{ - Channel, ChatMessage, DmConversation, FriendState, GuildState, NodeBookmark, SettingsPage, -}; -pub use types::{ConnState, TYPING_TIMEOUT, UiState, VoiceUiState}; - -use std::time::{Duration, Instant}; +pub use models::{Channel, ChatMessage, DmConversation, FriendState, GuildState, NodeBookmark}; +pub use types::{ConnState, UiState, VoiceUiState}; use models::bookmark_label_from_address; impl UiState { - pub fn refresh_audio_devices(&mut self) { - let now = Instant::now(); - if let Some(last) = self.last_device_scan - && now.duration_since(last) < Duration::from_millis(2500) - { - return; - } - self.last_device_scan = Some(now); - - match vnox_client::audio::devices::enumerate() { - Ok(lists) => { - self.input_device_labels = lists.input_labels; - self.input_device_metas = lists.input_devices; - self.output_device_labels = lists.output_labels; - self.output_device_metas = lists.output_devices; - } - Err(e) => { - tracing::warn!("audio device enumeration: {e}"); - } - } - - if self.input_device_labels.is_empty() { - self.input_device_labels.push("Default".into()); - } - if self.output_device_labels.is_empty() { - self.output_device_labels.push("Default".into()); - } - - self.mic_device = self - .mic_device - .min(self.input_device_labels.len().saturating_sub(1)); - self.output_device = self - .output_device - .min(self.output_device_labels.len().saturating_sub(1)); - } - - pub fn force_refresh_audio_devices(&mut self) { - self.last_device_scan = None; - self.refresh_audio_devices(); - } - pub fn remove_bookmark(&mut self, index: usize) { if index < self.bookmarks.len() { self.bookmarks.remove(index); @@ -73,11 +28,6 @@ impl UiState { true } - pub fn voice_elapsed(&self) -> Option { - let secs = self.voice_joined_at?.elapsed().as_secs(); - Some(format!("{}:{:02}", secs / 60, secs % 60)) - } - pub fn nick_for<'a>(&'a self, sender_id: &'a str) -> &'a str { self.user_names .get(sender_id) @@ -85,12 +35,6 @@ impl UiState { .unwrap_or(sender_id) } - pub fn active_voice_channel(&self) -> Option<&Channel> { - self.channels - .iter() - .find(|c| matches!(&self.voice, VoiceUiState::Active { channel } if channel == &c.id)) - } - pub fn seed_default_channels(&mut self) { if !self.channels.is_empty() { return; diff --git a/src/ui/state/models.rs b/src/ui/state/models.rs index 5a02528..c279dd3 100644 --- a/src/ui/state/models.rs +++ b/src/ui/state/models.rs @@ -1,19 +1,5 @@ use std::collections::HashMap; -#[derive(Debug, Clone, PartialEq, Default)] -pub enum SettingsPage { - Identity, - #[default] - Voice, - AudioOutput, - Network, - Overlay, - Appearance, - Keybinds, - Plugins, - Advanced, -} - #[derive(Debug, Clone)] pub struct Channel { pub id: String, @@ -38,16 +24,17 @@ pub struct ChatMessage { #[derive(Debug, Clone)] pub struct DmConversation { pub dm_id: String, - pub other_user_id: String, pub other_nickname: String, pub unread_count: u32, + pub e2ee_enabled: bool, + pub e2ee_peer_public_key: Option>, + pub e2ee_shared_secret: Option<[u8; 32]>, } #[derive(Debug, Clone)] pub struct GuildState { pub guild_id: String, pub name: String, - pub owner_id: String, pub member_count: u32, } @@ -55,8 +42,6 @@ pub struct GuildState { pub struct FriendState { pub user_id: String, pub nickname: String, - pub status: String, - pub since: i64, } #[derive(Debug, Clone, PartialEq, Eq)] @@ -79,14 +64,3 @@ pub(crate) fn bookmark_label_from_address(address: &str) -> String { .take(12) .collect() } - -pub(crate) fn now_utc_hms() -> String { - let secs = std::time::SystemTime::now() - .duration_since(std::time::UNIX_EPOCH) - .map(|d| d.as_secs()) - .unwrap_or(0); - let h = (secs % 86400) / 3600; - let m = (secs % 3600) / 60; - let s = secs % 60; - format!("{h:02}:{m:02}:{s:02}") -} diff --git a/src/ui/state/types.rs b/src/ui/state/types.rs index 2939974..7471e08 100644 --- a/src/ui/state/types.rs +++ b/src/ui/state/types.rs @@ -1,7 +1,5 @@ -use std::collections::{HashMap, HashSet}; -use std::time::{Duration, Instant}; - -pub const TYPING_TIMEOUT: Duration = Duration::from_secs(4); +use std::collections::HashMap; +use std::time::Instant; #[derive(Debug, Clone, PartialEq, Default)] pub enum VoiceUiState { @@ -37,169 +35,54 @@ pub struct UiState { pub active_channel: Option, pub messages: HashMap>, pub chat_input: String, - pub editing_message_id: Option, - /// When set, the next sent message will be a reply to this message_id. - pub replying_to: Option<(String, String)>, // (message_id, sender_id) - /// Message currently shown in the context menu (right-click). - pub context_menu_message_id: Option, pub scroll_bottom: bool, pub voice: VoiceUiState, pub voice_joined_at: Option, pub user_names: HashMap, - pub server_channel: Option, pub rtt_ms: Option, pub settings_open: bool, - pub settings_page: super::SettingsPage, pub mic_enabled: bool, pub deafen: bool, - pub noise_suppress: bool, - pub echo_cancel: bool, pub vad_mode: usize, - pub vad_threshold: f32, pub voice_bitrate: f32, pub packet_interval: usize, pub mic_device: usize, - pub input_volume: f32, - pub input_device_labels: Vec, - pub output_device_labels: Vec, - pub input_device_metas: Vec, - pub output_device_metas: Vec, - pub last_device_scan: Option, pub output_device: usize, pub output_volume: f32, - pub jitter_size: usize, - pub adaptive_buffer: bool, - pub overlay_enabled: bool, - pub overlay_speakers: bool, - pub overlay_latency: bool, - - pub relay_enabled: bool, - pub relay_address: String, - pub auto_relay: bool, - pub force_relay: bool, - pub udp_port: String, pub auto_reconnect: bool, - pub ui_scale: f32, - - pub kb_ptt: String, - pub kb_mute: String, - pub kb_deafen: String, - pub kb_overlay: String, - - pub log_level: usize, - pub log_voice_packets: bool, - pub show_packet_stats: bool, - pub disable_encryption: bool, - pub private_mode: bool, pub guilds: Vec, pub active_guild_id: Option, pub presences: HashMap, - pub presence_status: Option, - /// Free-form custom status text ("making tea"). - pub custom_status: Option, - /// Activity type ("playing", "listening", "watching", "streaming"). - pub activity_type: Option, - /// Activity text (e.g. "Counter-Strike 2"). - pub activity_text: Option, pub friends: Vec, pub pending_friend_requests: Vec, - /// Blocked user IDs. pub blocked_users: Vec, - /// Input buffer for the block-user text field. - pub block_input: Option, - /// When the blocked list was last fetched from the server. - pub block_list_fetched_at: Option, pub dm_conversations: Vec, pub dm_messages: HashMap>, pub active_dm_id: Option, pub new_dm_input: Option, - pub dm_search_query: String, - pub user_colors: HashMap, - - pub per_user_volumes: HashMap, - - pub add_server_open: bool, - pub add_server_input: String, - pub invite_accept_open: bool, - pub invite_accept_code: String, - - pub create_guild_open: bool, - pub create_guild_input: String, - - /// "Create a Channel" popup state. - pub create_channel_open: bool, - pub create_channel_input: String, - pub create_channel_kind: String, - - /// Vault UI state — set/remove passphrase modals. - pub vault_set_open: bool, - pub vault_remove_open: bool, - pub vault_passphrase_input: String, - pub vault_passphrase_confirm: String, - pub vault_error: Option, - - /// Export/import keyfile UI state. - pub export_keyfile_open: bool, - pub export_keyfile_pass: String, - pub export_keyfile_confirm: String, - pub export_keyfile_output: String, - pub export_keyfile_error: Option, - pub import_keyfile_open: bool, - pub import_keyfile_input: String, - pub import_keyfile_pass: String, - pub import_keyfile_error: Option, - - /// Audit log viewer state. - pub audit_log_open: bool, - pub audit_log_guild_id: Option, - pub audit_log_entries: Vec, - - /// Guild member list viewer state. - pub guild_members_open: bool, - pub guild_member_list_guild_id: Option, - pub guild_member_list: Vec, - - /// Guild role list state. - pub guild_role_list_guild_id: Option, - pub guild_role_list: Vec, - - pub add_friend_open: bool, - pub add_friend_input: String, - - pub kb_listening: Option, - - pub collapsed_categories: HashSet, - - pub startup_time_str: String, + pub user_colors: HashMap, pub typing_users: HashMap>, - pub last_typing_send: Instant, pub read_receipts: HashMap>, - /// Active tab in the Friends panel ("online" / "all" / "pending" / "blocked"). pub friends_tab: String, - /// True when local user is transmitting voice (PTT held or VAD above threshold). pub local_speaking: bool, - /// True when a remote peer's voice packet arrived in the last ~500ms. pub remote_speaking: bool, - /// Most recently received voice-packet sender's user_id (hex pubkey). - /// Empty when no remote user has spoken recently. pub last_remote_speaker_id: String, - /// UNIX millisecond timestamp of the last voice packet from any remote peer. pub last_voice_packet_ms: u64, } diff --git a/src/ui/theme/icons.rs b/src/ui/theme/icons.rs deleted file mode 100644 index 493778b..0000000 --- a/src/ui/theme/icons.rs +++ /dev/null @@ -1,710 +0,0 @@ -//! Hand-drawn vector icons rendered via `egui::Painter`. -//! -//! Each icon is a function `draw_(painter, rect, color)` that fills -//! the given `egui::Rect` with a monochrome glyph of the given `Color32`. -//! -//! All glyphs are designed on a 24×24 grid normalized to the rect. -//! Replace emoji usage throughout the UI with these for a cleaner, more -//! professional look that doesn't depend on system emoji fonts. - -use eframe::egui::{self, Color32, Pos2, Rect, Shape, Stroke, Vec2}; - -/// Draw an icon centered in the given rect, scaled to fit. -pub fn draw_icon( - ui: &mut egui::Ui, - size: f32, - draw_fn: impl FnOnce(&egui::Painter, Rect, Color32), -) -> egui::Response { - let (rect, resp) = ui.allocate_exact_size(egui::vec2(size, size), egui::Sense::hover()); - if ui.is_rect_visible(rect) { - let painter = ui.painter(); - let s = rect.size().min_elem(); - let center = rect.center(); - let half = s / 2.0; - let scaled = - Rect::from_min_size(Pos2::new(center.x - half, center.y - half), Vec2::splat(s)); - draw_fn(painter, scaled, super::TEXT_MUTED); - } - resp -} - -/// Helper to convert a 0..24 coordinate into the scaled rect's coordinate. -fn p(rect: Rect, x: f32, y: f32) -> Pos2 { - let sx = rect.width() / 24.0; - let sy = rect.height() / 24.0; - Pos2::new(rect.min.x + x * sx, rect.min.y + y * sy) -} - -/// Microphone icon. -pub fn microphone(painter: &egui::Painter, rect: Rect, color: Color32) { - // Mic body (rounded capsule). - let body = Rect::from_center_size( - p(rect, 12.0, 9.0), - Vec2::new(rect.width() * 0.18, rect.height() * 0.42), - ); - painter.rect_filled(body, egui::CornerRadius::same(4), color); - // Arc below (the cradle). - let arc_center = p(rect, 12.0, 14.0); - let radius = rect.width() * 0.22; - painter.add(Shape::circle_stroke( - arc_center, - radius, - Stroke::new(rect.width() * 0.05, color), - )); - // Stand. - painter.line_segment( - [p(rect, 12.0, 16.0), p(rect, 12.0, 20.0)], - Stroke::new(rect.width() * 0.06, color), - ); - painter.line_segment( - [p(rect, 9.0, 20.0), p(rect, 15.0, 20.0)], - Stroke::new(rect.width() * 0.06, color), - ); -} - -/// Microphone-slash icon (mic with a diagonal line through it). -pub fn microphone_off(painter: &egui::Painter, rect: Rect, color: Color32) { - microphone(painter, rect, Color32::from_rgb(80, 80, 80)); - painter.line_segment( - [p(rect, 5.0, 5.0), p(rect, 19.0, 19.0)], - Stroke::new(rect.width() * 0.08, color), - ); -} - -/// Headphones icon (for deafen). -pub fn headphones(painter: &egui::Painter, rect: Rect, color: Color32) { - // Top arc. - painter.add(Shape::circle_stroke( - p(rect, 12.0, 13.0), - rect.width() * 0.30, - Stroke::new(rect.width() * 0.06, color), - )); - // Cover bottom half of the arc so only top shows. - painter.rect_filled( - Rect::from_min_max(p(rect, 4.0, 13.0), p(rect, 20.0, 21.0)), - egui::CornerRadius::same(0), - super::BG_ELEVATED, - ); - // Left ear cup. - painter.rect_filled( - Rect::from_center_size( - p(rect, 5.5, 13.0), - Vec2::new(rect.width() * 0.18, rect.height() * 0.28), - ), - egui::CornerRadius::same(2), - color, - ); - // Right ear cup. - painter.rect_filled( - Rect::from_center_size( - p(rect, 18.5, 13.0), - Vec2::new(rect.width() * 0.18, rect.height() * 0.28), - ), - egui::CornerRadius::same(2), - color, - ); -} - -/// Headphones-off (with slash). -pub fn headphones_off(painter: &egui::Painter, rect: Rect, color: Color32) { - headphones(painter, rect, Color32::from_rgb(80, 80, 80)); - painter.line_segment( - [p(rect, 5.0, 5.0), p(rect, 19.0, 19.0)], - Stroke::new(rect.width() * 0.08, color), - ); -} - -/// Gear / settings icon (cog). -pub fn gear(painter: &egui::Painter, rect: Rect, color: Color32) { - let center = p(rect, 12.0, 12.0); - let outer_r = rect.width() * 0.40; - let inner_r = rect.width() * 0.28; - let hole_r = rect.width() * 0.12; - // Draw 8 teeth as small rounded rects around the circle. - for i in 0..8 { - let angle = i as f32 * std::f32::consts::TAU / 8.0; - let tooth_center = Pos2::new( - center.x + angle.cos() * (outer_r - rect.width() * 0.04), - center.y + angle.sin() * (outer_r - rect.width() * 0.04), - ); - painter.rect_filled( - Rect::from_center_size(tooth_center, Vec2::splat(rect.width() * 0.10)), - egui::CornerRadius::same(1), - color, - ); - } - // Outer ring. - painter.add(Shape::circle_stroke( - center, - inner_r, - Stroke::new(rect.width() * 0.07, color), - )); - // Center hole. - painter.circle_filled(center, hole_r, color); -} - -/// Plus icon. -pub fn plus(painter: &egui::Painter, rect: Rect, color: Color32) { - let w = rect.width() * 0.08; - let h = rect.height() * 0.6; - let v = rect.width() * 0.6; - let hv = rect.height() * 0.08; - painter.rect_filled( - Rect::from_center_size(rect.center(), Vec2::new(w, h)), - egui::CornerRadius::same(1), - color, - ); - painter.rect_filled( - Rect::from_center_size(rect.center(), Vec2::new(v, hv)), - egui::CornerRadius::same(1), - color, - ); -} - -/// X / close icon. -pub fn close(painter: &egui::Painter, rect: Rect, color: Color32) { - let stroke = Stroke::new(rect.width() * 0.08, color); - painter.line_segment([p(rect, 6.0, 6.0), p(rect, 18.0, 18.0)], stroke); - painter.line_segment([p(rect, 18.0, 6.0), p(rect, 6.0, 18.0)], stroke); -} - -/// Trash icon. -pub fn trash(painter: &egui::Painter, rect: Rect, color: Color32) { - let w = rect.width() * 0.06; - // Lid. - painter.line_segment( - [p(rect, 5.0, 7.0), p(rect, 19.0, 7.0)], - Stroke::new(w, color), - ); - // Handle. - painter.line_segment( - [p(rect, 10.0, 5.0), p(rect, 14.0, 5.0)], - Stroke::new(w, color), - ); - // Body. - painter.line_segment( - [p(rect, 7.0, 7.0), p(rect, 8.5, 19.0)], - Stroke::new(w, color), - ); - painter.line_segment( - [p(rect, 17.0, 7.0), p(rect, 15.5, 19.0)], - Stroke::new(w, color), - ); - painter.line_segment( - [p(rect, 8.5, 19.0), p(rect, 15.5, 19.0)], - Stroke::new(w, color), - ); - // Ribs. - painter.line_segment( - [p(rect, 11.0, 9.0), p(rect, 11.0, 17.0)], - Stroke::new(w * 0.7, color), - ); - painter.line_segment( - [p(rect, 13.0, 9.0), p(rect, 13.0, 17.0)], - Stroke::new(w * 0.7, color), - ); -} - -/// Lock icon (closed padlock). -pub fn lock(painter: &egui::Painter, rect: Rect, color: Color32) { - // Shackle (arc). - painter.add(Shape::circle_stroke( - p(rect, 12.0, 9.0), - rect.width() * 0.18, - Stroke::new(rect.width() * 0.07, color), - )); - // Cover the bottom of the shackle arc. - painter.rect_filled( - Rect::from_min_max(p(rect, 9.0, 9.0), p(rect, 15.0, 13.0)), - egui::CornerRadius::same(0), - super::BG_ELEVATED, - ); - // Body. - painter.rect_filled( - Rect::from_center_size( - p(rect, 12.0, 15.0), - Vec2::new(rect.width() * 0.42, rect.height() * 0.28), - ), - egui::CornerRadius::same(2), - color, - ); -} - -/// Unlock icon (open padlock). -pub fn unlock(painter: &egui::Painter, rect: Rect, color: Color32) { - // Shackle (open — arc shifted right). - painter.add(Shape::circle_stroke( - p(rect, 14.0, 9.0), - rect.width() * 0.18, - Stroke::new(rect.width() * 0.07, color), - )); - // Cover bottom of shackle. - painter.rect_filled( - Rect::from_min_max(p(rect, 11.0, 9.0), p(rect, 17.0, 13.0)), - egui::CornerRadius::same(0), - super::BG_ELEVATED, - ); - // Body. - painter.rect_filled( - Rect::from_center_size( - p(rect, 10.0, 15.0), - Vec2::new(rect.width() * 0.42, rect.height() * 0.28), - ), - egui::CornerRadius::same(2), - color, - ); -} - -/// Refresh / rotate icon (circular arrow). -pub fn refresh(painter: &egui::Painter, rect: Rect, color: Color32) { - let center = p(rect, 12.0, 12.0); - let r = rect.width() * 0.32; - let stroke = Stroke::new(rect.width() * 0.07, color); - // Arc (3/4 circle). - let steps = 24; - let start = -std::f32::consts::FRAC_PI_2; - let end = start + std::f32::consts::TAU * 0.75; - let mut prev: Option = None; - for i in 0..=steps { - let t = start + (end - start) * (i as f32 / steps as f32); - let pt = Pos2::new(center.x + t.cos() * r, center.y + t.sin() * r); - if let Some(p0) = prev { - painter.line_segment([p0, pt], stroke); - } - prev = Some(pt); - } - // Arrowhead at the end. - let arrow_end = prev.unwrap(); - let perp = end + std::f32::consts::FRAC_PI_2; - let ah1 = Pos2::new( - arrow_end.x + perp.cos() * rect.width() * 0.10, - arrow_end.y + perp.sin() * rect.height() * 0.10, - ); - let ah2 = Pos2::new( - arrow_end.x - perp.cos() * rect.width() * 0.10, - arrow_end.y - perp.sin() * rect.height() * 0.10, - ); - painter.line_segment([arrow_end, ah1], stroke); - painter.line_segment([arrow_end, ah2], stroke); -} - -/// Clipboard / list icon (for audit log). -pub fn clipboard(painter: &egui::Painter, rect: Rect, color: Color32) { - let w = rect.width() * 0.06; - // Outer rect. - painter.rect_stroke( - Rect::from_min_max(p(rect, 5.0, 4.0), p(rect, 19.0, 20.0)), - egui::CornerRadius::same(2), - Stroke::new(w, color), - egui::StrokeKind::Middle, - ); - // Clip at top. - painter.rect_filled( - Rect::from_min_max(p(rect, 9.0, 3.0), p(rect, 15.0, 6.0)), - egui::CornerRadius::same(1), - color, - ); - // Lines. - painter.line_segment( - [p(rect, 8.0, 10.0), p(rect, 16.0, 10.0)], - Stroke::new(w, color), - ); - painter.line_segment( - [p(rect, 8.0, 13.0), p(rect, 16.0, 13.0)], - Stroke::new(w, color), - ); - painter.line_segment( - [p(rect, 8.0, 16.0), p(rect, 14.0, 16.0)], - Stroke::new(w, color), - ); -} - -/// People / members icon (two overlapping circles + shoulders). -pub fn people(painter: &egui::Painter, rect: Rect, color: Color32) { - let r1 = rect.width() * 0.16; - let r2 = rect.width() * 0.13; - // Two heads. - painter.circle_filled(p(rect, 9.0, 8.0), r1, color); - painter.circle_filled(p(rect, 16.0, 9.0), r2, color); - // Shoulders. - painter.rect_filled( - Rect::from_min_max(p(rect, 4.0, 13.0), p(rect, 14.0, 21.0)), - egui::CornerRadius::same(4), - color, - ); - painter.rect_filled( - Rect::from_min_max(p(rect, 12.0, 14.0), p(rect, 20.0, 21.0)), - egui::CornerRadius::same(4), - color, - ); -} - -/// Crown icon (for owner). -pub fn crown(painter: &egui::Painter, rect: Rect, color: Color32) { - let w = rect.width() * 0.06; - // Base bar. - painter.line_segment( - [p(rect, 5.0, 18.0), p(rect, 19.0, 18.0)], - Stroke::new(w * 1.5, color), - ); - // Three peaks. - painter.line_segment( - [p(rect, 5.0, 18.0), p(rect, 7.0, 8.0)], - Stroke::new(w, color), - ); - painter.line_segment( - [p(rect, 7.0, 8.0), p(rect, 12.0, 14.0)], - Stroke::new(w, color), - ); - painter.line_segment( - [p(rect, 12.0, 14.0), p(rect, 17.0, 8.0)], - Stroke::new(w, color), - ); - painter.line_segment( - [p(rect, 17.0, 8.0), p(rect, 19.0, 18.0)], - Stroke::new(w, color), - ); - // Gems. - painter.circle_filled(p(rect, 7.0, 7.0), rect.width() * 0.04, color); - painter.circle_filled(p(rect, 17.0, 7.0), rect.width() * 0.04, color); - painter.circle_filled(p(rect, 12.0, 13.0), rect.width() * 0.04, color); -} - -/// Boot icon (for kick). -pub fn boot(painter: &egui::Painter, rect: Rect, color: Color32) { - let w = rect.width() * 0.07; - // Boot outline. - painter.line_segment( - [p(rect, 8.0, 5.0), p(rect, 8.0, 14.0)], - Stroke::new(w * 1.5, color), - ); - painter.line_segment( - [p(rect, 8.0, 14.0), p(rect, 8.0, 17.0)], - Stroke::new(w * 1.5, color), - ); - painter.line_segment( - [p(rect, 7.0, 17.0), p(rect, 18.0, 17.0)], - Stroke::new(w * 1.5, color), - ); - painter.line_segment( - [p(rect, 18.0, 17.0), p(rect, 18.0, 14.0)], - Stroke::new(w * 1.5, color), - ); - painter.line_segment( - [p(rect, 18.0, 14.0), p(rect, 12.0, 14.0)], - Stroke::new(w * 1.5, color), - ); - painter.line_segment( - [p(rect, 12.0, 14.0), p(rect, 12.0, 5.0)], - Stroke::new(w * 1.5, color), - ); - // Top opening. - painter.line_segment( - [p(rect, 8.0, 5.0), p(rect, 12.0, 5.0)], - Stroke::new(w, color), - ); -} - -/// Speaker (sound waves) icon — used for voice activity. -pub fn speaker(painter: &egui::Painter, rect: Rect, color: Color32) { - let w = rect.width() * 0.06; - // Speaker box. - painter.line_segment( - [p(rect, 4.0, 10.0), p(rect, 8.0, 10.0)], - Stroke::new(w, color), - ); - painter.line_segment( - [p(rect, 4.0, 14.0), p(rect, 8.0, 14.0)], - Stroke::new(w, color), - ); - painter.line_segment( - [p(rect, 4.0, 10.0), p(rect, 4.0, 14.0)], - Stroke::new(w, color), - ); - painter.line_segment( - [p(rect, 8.0, 10.0), p(rect, 12.0, 6.0)], - Stroke::new(w, color), - ); - painter.line_segment( - [p(rect, 8.0, 14.0), p(rect, 12.0, 18.0)], - Stroke::new(w, color), - ); - painter.line_segment( - [p(rect, 12.0, 6.0), p(rect, 12.0, 18.0)], - Stroke::new(w, color), - ); - // Sound waves. - painter.add(Shape::circle_stroke( - p(rect, 15.0, 12.0), - rect.width() * 0.10, - Stroke::new(w * 0.8, color), - )); - painter.add(Shape::circle_stroke( - p(rect, 17.0, 12.0), - rect.width() * 0.16, - Stroke::new(w * 0.6, color), - )); -} - -/// Speaker-muted icon (speaker with X). -pub fn speaker_off(painter: &egui::Painter, rect: Rect, color: Color32) { - speaker(painter, rect, Color32::from_rgb(80, 80, 80)); - let w = rect.width() * 0.07; - painter.line_segment( - [p(rect, 14.0, 8.0), p(rect, 20.0, 16.0)], - Stroke::new(w, color), - ); - painter.line_segment( - [p(rect, 20.0, 8.0), p(rect, 14.0, 16.0)], - Stroke::new(w, color), - ); -} - -/// Envelope icon (for DM). -pub fn envelope(painter: &egui::Painter, rect: Rect, color: Color32) { - let w = rect.width() * 0.06; - // Outer rect. - painter.rect_stroke( - Rect::from_min_max(p(rect, 4.0, 6.0), p(rect, 20.0, 18.0)), - egui::CornerRadius::same(1), - Stroke::new(w, color), - egui::StrokeKind::Middle, - ); - // Flap. - painter.line_segment( - [p(rect, 4.0, 6.0), p(rect, 12.0, 13.0)], - Stroke::new(w, color), - ); - painter.line_segment( - [p(rect, 20.0, 6.0), p(rect, 12.0, 13.0)], - Stroke::new(w, color), - ); -} - -/// Pencil / edit icon. -pub fn pencil(painter: &egui::Painter, rect: Rect, color: Color32) { - let w = rect.width() * 0.06; - // Diagonal pencil body. - painter.line_segment( - [p(rect, 5.0, 19.0), p(rect, 16.0, 8.0)], - Stroke::new(w * 1.5, color), - ); - // Tip. - painter.line_segment( - [p(rect, 4.0, 20.0), p(rect, 6.0, 18.0)], - Stroke::new(w * 1.5, color), - ); - // Eraser end. - painter.line_segment( - [p(rect, 15.0, 7.0), p(rect, 18.0, 10.0)], - Stroke::new(w * 1.5, color), - ); - painter.line_segment( - [p(rect, 16.0, 6.0), p(rect, 19.0, 9.0)], - Stroke::new(w * 1.5, color), - ); -} - -/// Reply arrow icon. -pub fn reply(painter: &egui::Painter, rect: Rect, color: Color32) { - let w = rect.width() * 0.07; - // Curved arrow (drawn as two segments). - painter.line_segment( - [p(rect, 16.0, 8.0), p(rect, 8.0, 8.0)], - Stroke::new(w, color), - ); - painter.line_segment( - [p(rect, 8.0, 8.0), p(rect, 8.0, 16.0)], - Stroke::new(w, color), - ); - // Arrowhead. - painter.line_segment( - [p(rect, 5.0, 11.0), p(rect, 8.0, 8.0)], - Stroke::new(w, color), - ); - painter.line_segment( - [p(rect, 8.0, 8.0), p(rect, 11.0, 11.0)], - Stroke::new(w, color), - ); -} - -/// Copy icon (two overlapping squares). -pub fn copy(painter: &egui::Painter, rect: Rect, color: Color32) { - let w = rect.width() * 0.06; - // Back square. - painter.rect_stroke( - Rect::from_min_max(p(rect, 8.0, 4.0), p(rect, 20.0, 16.0)), - egui::CornerRadius::same(1), - Stroke::new(w, color), - egui::StrokeKind::Middle, - ); - // Front square. - painter.rect_stroke( - Rect::from_min_max(p(rect, 4.0, 8.0), p(rect, 16.0, 20.0)), - egui::CornerRadius::same(1), - Stroke::new(w, color), - egui::StrokeKind::Middle, - ); -} - -/// User-plus icon (for "add friend"). -pub fn user_plus(painter: &egui::Painter, rect: Rect, color: Color32) { - let w = rect.width() * 0.06; - // Head. - painter.circle_filled(p(rect, 9.0, 8.0), rect.width() * 0.14, color); - // Shoulders. - painter.rect_filled( - Rect::from_min_max(p(rect, 4.0, 13.0), p(rect, 14.0, 21.0)), - egui::CornerRadius::same(4), - color, - ); - // Plus. - painter.line_segment( - [p(rect, 16.0, 11.0), p(rect, 20.0, 11.0)], - Stroke::new(w, color), - ); - painter.line_segment( - [p(rect, 18.0, 9.0), p(rect, 18.0, 13.0)], - Stroke::new(w, color), - ); -} - -/// Home icon. -pub fn home(painter: &egui::Painter, rect: Rect, color: Color32) { - let w = rect.width() * 0.06; - // Roof. - painter.line_segment( - [p(rect, 4.0, 12.0), p(rect, 12.0, 5.0)], - Stroke::new(w, color), - ); - painter.line_segment( - [p(rect, 12.0, 5.0), p(rect, 20.0, 12.0)], - Stroke::new(w, color), - ); - // Body. - painter.line_segment( - [p(rect, 6.0, 12.0), p(rect, 6.0, 19.0)], - Stroke::new(w, color), - ); - painter.line_segment( - [p(rect, 18.0, 12.0), p(rect, 18.0, 19.0)], - Stroke::new(w, color), - ); - painter.line_segment( - [p(rect, 6.0, 19.0), p(rect, 18.0, 19.0)], - Stroke::new(w, color), - ); - // Door. - painter.line_segment( - [p(rect, 10.0, 19.0), p(rect, 10.0, 14.0)], - Stroke::new(w, color), - ); - painter.line_segment( - [p(rect, 14.0, 19.0), p(rect, 14.0, 14.0)], - Stroke::new(w, color), - ); - painter.line_segment( - [p(rect, 10.0, 14.0), p(rect, 14.0, 14.0)], - Stroke::new(w, color), - ); -} - -/// Link icon (for invites). -pub fn link(painter: &egui::Painter, rect: Rect, color: Color32) { - let w = rect.width() * 0.07; - // Left chain. - painter.rect_stroke( - Rect::from_min_max(p(rect, 3.0, 8.0), p(rect, 12.0, 16.0)), - egui::CornerRadius::same(4), - Stroke::new(w, color), - egui::StrokeKind::Middle, - ); - // Right chain. - painter.rect_stroke( - Rect::from_min_max(p(rect, 12.0, 8.0), p(rect, 21.0, 16.0)), - egui::CornerRadius::same(4), - Stroke::new(w, color), - egui::StrokeKind::Middle, - ); - // Connector. - painter.line_segment( - [p(rect, 9.0, 12.0), p(rect, 15.0, 12.0)], - Stroke::new(w, color), - ); -} - -/// Tag icon (for roles). -pub fn tag(painter: &egui::Painter, rect: Rect, color: Color32) { - let w = rect.width() * 0.06; - // Tag body. - let pts = [ - p(rect, 4.0, 12.0), - p(rect, 8.0, 6.0), - p(rect, 19.0, 6.0), - p(rect, 19.0, 18.0), - p(rect, 8.0, 18.0), - ]; - for i in 0..pts.len() - 1 { - painter.line_segment([pts[i], pts[i + 1]], Stroke::new(w, color)); - } - painter.line_segment([pts[pts.len() - 1], pts[0]], Stroke::new(w, color)); - // Hole. - painter.circle_filled(p(rect, 15.0, 12.0), rect.width() * 0.04, color); -} - -/// Check / tick icon. -pub fn check(painter: &egui::Painter, rect: Rect, color: Color32) { - painter.line_segment( - [p(rect, 5.0, 12.0), p(rect, 10.0, 17.0)], - Stroke::new(rect.width() * 0.10, color), - ); - painter.line_segment( - [p(rect, 10.0, 17.0), p(rect, 19.0, 6.0)], - Stroke::new(rect.width() * 0.10, color), - ); -} - -/// Hash icon (for text channel). -pub fn hash(painter: &egui::Painter, rect: Rect, color: Color32) { - let w = rect.width() * 0.07; - painter.line_segment( - [p(rect, 8.0, 4.0), p(rect, 6.0, 20.0)], - Stroke::new(w, color), - ); - painter.line_segment( - [p(rect, 16.0, 4.0), p(rect, 14.0, 20.0)], - Stroke::new(w, color), - ); - painter.line_segment( - [p(rect, 4.0, 9.0), p(rect, 18.0, 9.0)], - Stroke::new(w, color), - ); - painter.line_segment( - [p(rect, 3.0, 15.0), p(rect, 17.0, 15.0)], - Stroke::new(w, color), - ); -} - -/// Waveform icon (for voice channel). -pub fn waveform(painter: &egui::Painter, rect: Rect, color: Color32) { - let w = rect.width() * 0.07; - let cy = rect.center().y; - let h = rect.height(); - let bars = [ - (6.0_f32, 0.25_f32), - (9.0, 0.55), - (12.0, 0.85), - (15.0, 0.55), - (18.0, 0.25), - ]; - for (x, frac) in bars { - let bh = h * frac * 0.7; - painter.line_segment( - [ - Pos2::new(rect.min.x + x * rect.width() / 24.0, cy - bh / 2.0), - Pos2::new(rect.min.x + x * rect.width() / 24.0, cy + bh / 2.0), - ], - Stroke::new(w, color), - ); - } -} diff --git a/src/ui/theme/mod.rs b/src/ui/theme/mod.rs deleted file mode 100644 index d6d45b8..0000000 --- a/src/ui/theme/mod.rs +++ /dev/null @@ -1,144 +0,0 @@ -pub mod icons; -pub mod widgets; - -pub use widgets::{avatar, icon_btn, icon_btn_drawn}; - -use eframe::egui::{self, Color32, CornerRadius, FontDefinitions, Stroke, Visuals}; - -pub const BG_BASE: Color32 = Color32::from_rgb(13, 13, 13); -pub const BG_SURFACE: Color32 = Color32::from_rgb(22, 22, 22); -pub const BG_ELEVATED: Color32 = Color32::from_rgb(28, 28, 28); -pub const BG_INTERACTIVE: Color32 = Color32::from_rgb(30, 30, 30); - -pub const ACCENT: Color32 = Color32::from_rgb(230, 126, 34); -pub const ACCENT_HOVER: Color32 = Color32::from_rgb(245, 145, 55); -pub const ACCENT_SOFT: Color32 = Color32::from_rgb(200, 110, 30); -pub const ACCENT_10: Color32 = Color32::from_rgba_premultiplied(230, 126, 34, 25); -pub const ACCENT_20: Color32 = Color32::from_rgba_premultiplied(230, 126, 34, 50); -pub const BORDER_ACCENT: Color32 = Color32::from_rgba_premultiplied(230, 126, 34, 60); - -pub const SUCCESS: Color32 = Color32::from_rgb(80, 180, 90); -pub const WARNING: Color32 = Color32::from_rgb(220, 170, 60); -pub const ERROR: Color32 = Color32::from_rgb(210, 90, 80); -pub const INFO: Color32 = Color32::from_rgb(90, 155, 210); - -pub const TEXT_PRIMARY: Color32 = Color32::from_rgb(224, 224, 224); -pub const TEXT_SECONDARY: Color32 = Color32::from_rgb(170, 170, 170); -pub const TEXT_MUTED: Color32 = Color32::from_rgb(136, 136, 136); -pub const TEXT_DISABLED: Color32 = Color32::from_rgb(90, 90, 90); -pub const TEXT_DEAD: Color32 = Color32::from_rgb(55, 55, 55); - -pub const BORDER_SUBTLE: Color32 = Color32::from_rgb(42, 42, 42); -pub const BORDER_DEFAULT: Color32 = Color32::from_rgb(51, 51, 51); -pub const BORDER_STRONG: Color32 = Color32::from_rgb(64, 64, 64); - -pub const BG_STRIP: Color32 = BG_SURFACE; -pub const BG_PANEL: Color32 = BG_SURFACE; -pub const BORDER: Color32 = BORDER_DEFAULT; -pub const BORDER_FAINT: Color32 = BORDER_SUBTLE; -pub const BORDER_MID: Color32 = BORDER_STRONG; -pub const WARN: Color32 = WARNING; -pub const DANGER: Color32 = ERROR; -pub const BLUE: Color32 = INFO; -pub const TEXT_GHOST: Color32 = TEXT_DISABLED; -pub const TEXT_DIM: Color32 = TEXT_MUTED; -pub const ACCENT_DIM: Color32 = Color32::from_rgb(160, 90, 25); -pub const TEXT_FAINT: Color32 = TEXT_DEAD; - -pub const R_SM: CornerRadius = CornerRadius::same(6); -pub const R_MD: CornerRadius = CornerRadius::same(10); -pub const R_LG: CornerRadius = CornerRadius::same(14); -pub const R_FULL: CornerRadius = CornerRadius::same(255); - -const NAME_PALETTE: [Color32; 6] = [ - Color32::from_rgb(170, 170, 170), - Color32::from_rgb(106, 158, 207), - Color32::from_rgb(124, 184, 122), - Color32::from_rgb(220, 170, 60), - Color32::from_rgb(190, 140, 200), - Color32::from_rgb(120, 190, 180), -]; - -pub fn user_color(user_id: &str) -> Color32 { - if user_id.is_empty() { - return TEXT_SECONDARY; - } - let hash: usize = user_id - .bytes() - .fold(0usize, |acc, b| acc.wrapping_add(b as usize)); - NAME_PALETTE[hash % NAME_PALETTE.len()] -} - -pub fn apply_visuals(ctx: &egui::Context) { - let mut v = Visuals::dark(); - - v.window_fill = BG_BASE; - v.panel_fill = BG_BASE; - v.extreme_bg_color = BG_INTERACTIVE; - v.faint_bg_color = BG_ELEVATED; - v.code_bg_color = BG_SURFACE; - v.hyperlink_color = ACCENT; - v.override_text_color = Some(TEXT_PRIMARY); - - v.widgets.noninteractive.bg_fill = BG_SURFACE; - v.widgets.noninteractive.bg_stroke = Stroke::new(1.0, BORDER_SUBTLE); - v.widgets.noninteractive.fg_stroke = Stroke::new(1.0, TEXT_MUTED); - v.widgets.noninteractive.corner_radius = R_MD; - - v.widgets.inactive.bg_fill = Color32::TRANSPARENT; - v.widgets.inactive.bg_stroke = Stroke::new(1.0, BORDER_DEFAULT); - v.widgets.inactive.fg_stroke = Stroke::new(1.0, TEXT_MUTED); - v.widgets.inactive.corner_radius = R_MD; - - v.widgets.hovered.bg_fill = BG_INTERACTIVE; - v.widgets.hovered.bg_stroke = Stroke::new(1.0, BORDER_STRONG); - v.widgets.hovered.fg_stroke = Stroke::new(1.0, TEXT_PRIMARY); - v.widgets.hovered.corner_radius = R_MD; - - v.widgets.active.bg_fill = BG_INTERACTIVE; - v.widgets.active.bg_stroke = Stroke::new(1.0, BORDER_ACCENT); - v.widgets.active.fg_stroke = Stroke::new(1.0, TEXT_PRIMARY); - v.widgets.active.corner_radius = R_MD; - - v.widgets.open.bg_fill = BG_INTERACTIVE; - v.widgets.open.bg_stroke = Stroke::new(1.0, BORDER_STRONG); - v.widgets.open.fg_stroke = Stroke::new(1.0, TEXT_PRIMARY); - v.widgets.open.corner_radius = R_MD; - - v.selection.bg_fill = ACCENT_10; - v.selection.stroke = Stroke::new(0.0, Color32::TRANSPARENT); - - v.window_corner_radius = R_LG; - v.window_shadow = egui::epaint::Shadow::NONE; - v.popup_shadow = egui::epaint::Shadow::NONE; - v.window_stroke = Stroke::new(1.0, BORDER_DEFAULT); - - ctx.set_visuals(v); - set_fonts(ctx); - set_spacing(ctx); -} - -fn set_fonts(ctx: &egui::Context) { - let fonts = FontDefinitions::default(); - ctx.set_fonts(fonts); -} - -fn set_spacing(ctx: &egui::Context) { - let mut s = (*ctx.style()).clone(); - s.text_styles = [ - (egui::TextStyle::Small, egui::FontId::proportional(11.0)), - (egui::TextStyle::Body, egui::FontId::proportional(13.0)), - (egui::TextStyle::Button, egui::FontId::proportional(13.0)), - (egui::TextStyle::Heading, egui::FontId::proportional(15.0)), - (egui::TextStyle::Monospace, egui::FontId::monospace(12.0)), - ] - .into(); - s.spacing.item_spacing = egui::vec2(6.0, 4.0); - s.spacing.button_padding = egui::vec2(10.0, 5.0); - s.spacing.window_margin = egui::Margin::same(0); - s.spacing.menu_margin = egui::Margin::same(4); - s.spacing.indent = 16.0; - s.spacing.interact_size = egui::vec2(40.0, 28.0); - s.spacing.scroll.bar_width = 4.0; - ctx.set_style(s); -} diff --git a/src/ui/theme/widgets.rs b/src/ui/theme/widgets.rs deleted file mode 100644 index f74d2d3..0000000 --- a/src/ui/theme/widgets.rs +++ /dev/null @@ -1,76 +0,0 @@ -use eframe::egui::{self, Color32, Rect, Stroke}; - -use super::*; - -pub fn avatar(ui: &mut egui::Ui, initials: &str, color: Color32, size: f32) -> egui::Response { - let (rect, resp) = ui.allocate_exact_size(egui::vec2(size, size), egui::Sense::hover()); - if ui.is_rect_visible(rect) { - let painter = ui.painter(); - painter.circle_filled(rect.center(), size / 2.0, color); - painter.circle_stroke(rect.center(), size / 2.0, Stroke::new(1.0, BORDER_DEFAULT)); - painter.text( - rect.center(), - egui::Align2::CENTER_CENTER, - initials, - egui::FontId::proportional(size * 0.38), - TEXT_PRIMARY, - ); - } - resp -} - -pub fn icon_btn( - ui: &mut egui::Ui, - icon: &str, - tooltip: &str, - active_color: Option, -) -> egui::Response { - let color = active_color.unwrap_or(TEXT_MUTED); - let btn = egui::Button::new(egui::RichText::new(icon).color(color).size(14.0)) - .frame(false) - .min_size(egui::vec2(26.0, 26.0)); - let resp = ui.add(btn); - if !tooltip.is_empty() { - resp.clone().on_hover_text(tooltip); - } - resp -} - -/// Draw a vector icon into a button-sized area. -/// `draw_fn` is one of the functions from `crate::theme::icons` (e.g. `icons::gear`). -/// The icon is rendered with the given color; pass `None` to use TEXT_MUTED. -pub fn icon_btn_drawn( - ui: &mut egui::Ui, - draw_fn: fn(&egui::Painter, Rect, Color32), - tooltip: &str, - active_color: Option, - size: f32, -) -> egui::Response { - let color = active_color.unwrap_or(TEXT_MUTED); - let (rect, resp) = ui.allocate_exact_size(egui::vec2(size, size), egui::Sense::click()); - if ui.is_rect_visible(rect) { - let painter = ui.painter(); - // Inset to give padding. - let pad = size * 0.10; - let inner = Rect::from_min_max( - (rect.min.x + pad, rect.min.y + pad).into(), - (rect.max.x - pad, rect.max.y - pad).into(), - ); - draw_fn(painter, inner, color); - } - if ui.is_rect_visible(rect) && resp.hovered() { - let painter = ui.painter(); - painter.rect_filled(rect, egui::CornerRadius::same(4), BG_ELEVATED); - // Re-draw the icon on top of the hover background. - let pad = size * 0.10; - let inner = Rect::from_min_max( - (rect.min.x + pad, rect.min.y + pad).into(), - (rect.max.x - pad, rect.max.y - pad).into(), - ); - draw_fn(painter, inner, color); - } - if !tooltip.is_empty() { - resp.clone().on_hover_text(tooltip); - } - resp -} diff --git a/src/ui/titlebar.rs b/src/ui/titlebar.rs deleted file mode 100644 index 294db68..0000000 --- a/src/ui/titlebar.rs +++ /dev/null @@ -1,93 +0,0 @@ -use eframe::egui::{self, Context, Frame, TopBottomPanel}; - -use crate::state::{ConnState, UiState}; -use crate::theme as t; - -pub const TITLEBAR_H: f32 = 30.0; - -pub(crate) fn show_titlebar(ctx: &Context, s: &mut UiState) { - TopBottomPanel::top("titlebar") - .exact_height(TITLEBAR_H) - .frame(Frame::NONE.fill(t::BG_SURFACE)) - .show_separator_line(false) - .show(ctx, |ui| { - ui.horizontal(|ui| { - ui.add_space(12.0); - - ui.with_layout( - egui::Layout::left_to_right(egui::Align::Center).with_main_justify(true), - |ui| { - let w = ui.available_width(); - if w.is_finite() && w > 0.0 { - ui.set_width(w); - } - let title = match &s.conn { - ConnState::Connected { node_name, .. } => { - format!("VNOX · {node_name}") - } - ConnState::Connecting => "VNOX · connecting…".into(), - ConnState::Reconnecting { attempt } => { - format!("VNOX · reconnect #{attempt}") - } - ConnState::Disconnected => "VNOX · offline".into(), - }; - ui.label( - egui::RichText::new(title) - .font(egui::FontId::monospace(11.0)) - .color(t::TEXT_MUTED), - ); - }, - ); - - if matches!(s.conn, ConnState::Connected { .. }) - && s.overlay_latency - && let Some(rtt) = s.rtt_ms - { - let color = if rtt < 80 { - t::SUCCESS - } else if rtt < 200 { - t::WARNING - } else { - t::ERROR - }; - ui.label( - egui::RichText::new(format!("{rtt} ms")) - .font(egui::FontId::monospace(10.0)) - .color(color), - ); - ui.add_space(8.0); - } - - let transport = match &s.conn { - ConnState::Connected { .. } => "quic/v1", - ConnState::Connecting => "connecting…", - ConnState::Reconnecting { .. } => "reconnecting…", - ConnState::Disconnected => "idle", - }; - ui.label( - egui::RichText::new(transport) - .font(egui::FontId::monospace(10.0)) - .color(t::TEXT_DISABLED), - ); - ui.add_space(10.0); - - let settings_color = if s.settings_open { - Some(t::ACCENT) - } else { - None - }; - if t::icon_btn_drawn( - ui, - crate::theme::icons::gear, - "Settings", - settings_color, - 18.0, - ) - .clicked() - { - s.settings_open = !s.settings_open; - } - ui.add_space(12.0); - }); - }); -} diff --git a/src/ui/update.rs b/src/ui/update.rs new file mode 100644 index 0000000..0a4305e --- /dev/null +++ b/src/ui/update.rs @@ -0,0 +1,806 @@ +use std::collections::HashMap; + +use vnox_client::audio::{IncomingVoice, PipelineConfig}; +use vnox_client::net::{ChatMsg, NetCommand, NetEvent, NetHandle, PresenceInfo}; + +use crate::ui::state::{ChatMessage, ConnState, UiState, VoiceUiState}; + +use crate::AppState; + +pub fn process_net_events(state: &mut AppState) { + while let Some(event) = state.net.try_recv() { + match event { + NetEvent::Connected { + session_id, + node_name, + restored, + private_mode, + } => { + state.ui.conn = ConnState::Connected { + session_id, + node_name: node_name.clone(), + }; + state.ui.private_mode = private_mode; + if restored { + state.ui.status_msg = Some("reconnected".into()); + if let Some(ch) = state.ui.active_channel.clone() { + let net = state.net.clone(); + tokio::spawn(async move { + net.send(NetCommand::JoinChannel { channel_id: ch }).await; + }); + } + } else { + state.ui.seed_default_channels(); + state.ui.active_channel = Some("general".into()); + state.ui.status_msg = None; + let net = state.net.clone(); + tokio::spawn(async move { + net.send(NetCommand::JoinChannel { + channel_id: "general".into(), + }) + .await; + }); + } + } + NetEvent::Disconnected { reason, will_retry } => { + state.ui.voice = VoiceUiState::Off; + state.audio = None; + state.voice_channel = None; + state.last_pipeline_config = None; + if will_retry { + state.ui.status_msg = Some(format!("connection lost: {reason}")); + } else { + state.ui.conn = ConnState::Disconnected; + state.ui.channels.clear(); + state.ui.active_channel = None; + state.ui.user_names.clear(); + state.ui.rtt_ms = None; + state.ui.status_msg = Some(format!("disconnected: {reason}")); + } + state.ui.dm_conversations.clear(); + state.ui.dm_messages.clear(); + state.ui.active_dm_id = None; + state.ui.new_dm_input = None; + state.ui.private_mode = false; + } + NetEvent::Reconnecting { + attempt, + delay_secs, + } => { + state.ui.voice = VoiceUiState::Off; + state.audio = None; + state.voice_channel = None; + state.last_pipeline_config = None; + state.ui.conn = ConnState::Reconnecting { attempt }; + state.ui.status_msg = + Some(format!("reconnecting in {delay_secs}s (attempt {attempt})")); + } + NetEvent::VoicePacket { + channel_id, + voice_seq, + timestamp, + opus_data, + sender_id, + .. + } => { + if !sender_id.is_empty() { + state.ui.last_remote_speaker_id = sender_id.clone(); + state.ui.last_voice_packet_ms = std::time::SystemTime::now() + .duration_since(std::time::UNIX_EPOCH) + .map(|d| d.as_millis() as u64) + .unwrap_or(0); + } + if !state.ui.deafen + && let Some(pipeline) = state.audio.as_ref() + { + pipeline.push_voice(IncomingVoice { + voice_seq, + timestamp, + channel_id, + opus_data, + }); + } + } + NetEvent::E2eeDmKeyExchange { + dm_id, + e2ee_public_key, + } => { + let (secret, public) = vnox_client::e2ee::derive_e2ee_keypair(&state.identity); + let shared = vnox_client::e2ee::compute_shared_secret(&secret, &e2ee_public_key); + if let Some(conv) = state + .ui + .dm_conversations + .iter_mut() + .find(|c| c.dm_id == dm_id) + { + conv.e2ee_enabled = true; + conv.e2ee_peer_public_key = Some(e2ee_public_key); + conv.e2ee_shared_secret = Some(shared); + } + let pubkey_bytes = public.to_bytes().to_vec(); + let net = state.net.clone(); + tokio::spawn(async move { + net.send(NetCommand::E2eeDmKeyExchange { + dm_id, + e2ee_public_key: pubkey_bytes, + }) + .await; + }); + } + NetEvent::E2eeDmKeyExchangeAck { dm_id } => { + if let Some(conv) = state + .ui + .dm_conversations + .iter_mut() + .find(|c| c.dm_id == dm_id) + { + conv.e2ee_enabled = true; + } + } + NetEvent::E2eeDmMessage { + dm_id, + sender_id, + ciphertext, + timestamp, + } => { + let shared = state + .ui + .dm_conversations + .iter() + .find(|c| c.dm_id == dm_id) + .and_then(|c| c.e2ee_shared_secret); + let Some(shared) = shared else { + continue; + }; + let msg_id = uuid::Uuid::new_v4().to_string(); + let content = vnox_client::e2ee::decrypt_message(&shared, &msg_id, &ciphertext); + state + .ui + .dm_messages + .entry(dm_id.clone()) + .or_default() + .push(ChatMessage { + message_id: msg_id, + sender_id, + content, + timestamp, + edited: false, + reactions: HashMap::new(), + reply_to: None, + }); + if state.ui.active_dm_id.as_deref() != Some(&dm_id) + && let Some(c) = state + .ui + .dm_conversations + .iter_mut() + .find(|c| c.dm_id == dm_id) + { + c.unread_count += 1; + } + } + NetEvent::E2eeDmHistory { dm_id, messages } => { + let shared = state + .ui + .dm_conversations + .iter() + .find(|c| c.dm_id == dm_id) + .and_then(|c| c.e2ee_shared_secret); + let Some(shared) = shared else { + continue; + }; + let decrypted: Vec = messages + .into_iter() + .map(|m| { + let msg_id = uuid::Uuid::new_v4().to_string(); + let content = + vnox_client::e2ee::decrypt_message(&shared, &msg_id, &m.ciphertext); + ChatMessage { + message_id: msg_id, + sender_id: m.sender_id, + content, + timestamp: m.timestamp, + edited: false, + reactions: HashMap::new(), + reply_to: None, + } + }) + .collect(); + state.ui.dm_messages.insert(dm_id, decrypted); + } + event => process_net_event(&mut state.ui, &state.net, event), + } + sync_voice_audio(state); + flush_encoded_voice(state); + } + if let Some(pipeline) = state.audio.as_ref() { + state.ui.local_speaking = pipeline.local_speaking(); + state.ui.remote_speaking = pipeline.remote_speaking(500); + } else { + state.ui.local_speaking = false; + state.ui.remote_speaking = false; + } + if !state.ui.remote_speaking { + state.ui.last_remote_speaker_id.clear(); + } +} + +fn process_net_event(ui: &mut UiState, net: &NetHandle, event: NetEvent) { + match &event { + NetEvent::ChannelState { .. } + | NetEvent::ChannelCreated { .. } + | NetEvent::ChannelDeleted { .. } + | NetEvent::ChannelListEvent { .. } + | NetEvent::ChatMessage { .. } + | NetEvent::ChatHistory { .. } + | NetEvent::UserJoin { .. } + | NetEvent::UserLeave { .. } + | NetEvent::Error { .. } + | NetEvent::ReactionAdded { .. } + | NetEvent::ReactionRemoved { .. } + | NetEvent::MessageEdited { .. } + | NetEvent::MessageDeleted { .. } + | NetEvent::DmStart { .. } + | NetEvent::DmMessage { .. } + | NetEvent::DmHistory { .. } + | NetEvent::ReadReceiptBroadcast { .. } + | NetEvent::UserColor { .. } => chat_handle(ui, net, event), + + NetEvent::GuildList { .. } + | NetEvent::GuildCreated { .. } + | NetEvent::GuildDeleted { .. } + | NetEvent::GuildMemberJoined { .. } + | NetEvent::GuildMemberLeft { .. } + | NetEvent::GuildMemberKicked { .. } + | NetEvent::InviteCreated { .. } + | NetEvent::InviteAccepted { .. } + | NetEvent::InviteDeleted { .. } + | NetEvent::RoleCreated { .. } + | NetEvent::RoleDeleted { .. } + | NetEvent::GuildAuditLog { .. } + | NetEvent::GuildMemberList { .. } + | NetEvent::GuildRoleList { .. } + | NetEvent::FriendList { .. } + | NetEvent::FriendRequested { .. } + | NetEvent::FriendAccepted { .. } + | NetEvent::FriendRemoved { .. } + | NetEvent::BlockList { .. } + | NetEvent::BlockedUser { .. } + | NetEvent::UnblockedUser { .. } => social_handle(ui, event), + + NetEvent::LatencyUpdate { .. } + | NetEvent::TypingStart { .. } + | NetEvent::PresenceSync { .. } + | NetEvent::PresenceUpdated { .. } => status_handle(ui, net, event), + + NetEvent::Connected { .. } + | NetEvent::Disconnected { .. } + | NetEvent::Reconnecting { .. } + | NetEvent::VoicePacket { .. } + | NetEvent::E2eeDmKeyExchange { .. } + | NetEvent::E2eeDmKeyExchangeAck { .. } + | NetEvent::E2eeDmMessage { .. } + | NetEvent::E2eeDmHistory { .. } => {} + } +} + +fn into_msg(m: ChatMsg) -> ChatMessage { + ChatMessage { + message_id: m.message_id, + sender_id: m.sender_id, + content: m.content, + timestamp: m.timestamp, + edited: false, + reactions: HashMap::new(), + reply_to: m.reply_to, + } +} + +fn parse_hex_color(hex: &str) -> Option { + let hex = hex.trim_start_matches('#'); + if hex.len() != 6 { + return None; + } + u32::from_str_radix(hex, 16).ok()?; + Some(hex.to_string()) +} + +fn chat_handle(ui: &mut UiState, net: &NetHandle, event: NetEvent) { + match event { + NetEvent::ChannelState { + channel_id, + channel_name, + kind, + members, + .. + } => { + let mems: Vec = members + .iter() + .map(|m| { + ui.user_names.insert(m.user_id.clone(), m.nickname.clone()); + m.nickname.clone() + }) + .collect(); + if let Some(ch) = ui.channels.iter_mut().find(|c| c.id == channel_id) { + ch.members = mems; + } else { + ui.channels.push(crate::ui::state::Channel { + id: channel_id.clone(), + name: channel_name, + kind: kind.clone(), + members: mems, + }); + } + if ui.active_channel.is_none() { + ui.active_channel = Some(channel_id.clone()); + } + let net = net.clone(); + let ch_id = channel_id; + tokio::spawn(async move { + net.send(NetCommand::ReadReceipt { + channel_id: ch_id, + last_read_message_id: uuid::Uuid::new_v4().to_string(), + }) + .await; + }); + } + NetEvent::ChannelCreated { + channel_id, + channel_name, + kind, + } => { + if !ui.channels.iter().any(|c| c.id == channel_id) { + ui.channels.push(crate::ui::state::Channel { + id: channel_id, + name: channel_name, + kind, + members: Vec::new(), + }); + } + } + NetEvent::ChannelDeleted { channel_id } => { + ui.channels.retain(|c| c.id != channel_id); + if ui.active_channel.as_deref() == Some(&channel_id) { + ui.active_channel = ui.channels.first().map(|c| c.id.clone()); + } + ui.messages.remove(&channel_id); + } + NetEvent::ChannelListEvent { channels } => { + let old: std::collections::HashMap = + std::mem::take(&mut ui.channels) + .into_iter() + .map(|c| (c.id.clone(), c)) + .collect(); + ui.channels = channels + .into_iter() + .map(|c| { + let members = old + .get(&c.channel_id) + .map(|o| o.members.clone()) + .unwrap_or_default(); + crate::ui::state::Channel { + id: c.channel_id, + name: c.channel_name, + kind: c.kind, + members, + } + }) + .collect(); + } + NetEvent::UserJoin { + channel_id, + user_id, + nickname, + .. + } => { + ui.user_names.insert(user_id, nickname.clone()); + if let Some(ch) = ui.channels.iter_mut().find(|c| c.id == channel_id) + && !ch.members.contains(&nickname) + { + ch.members.push(nickname); + } + } + NetEvent::UserLeave { + channel_id, + user_id, + } => { + if let Some(ch) = ui.channels.iter_mut().find(|c| c.id == channel_id) { + let nickname = ui.user_names.remove(&user_id); + ch.members.retain(|m| { + m != &user_id && nickname.as_deref().map(|n| m != n).unwrap_or(true) + }); + } + } + NetEvent::DmStart { + dm_id, + other_nickname, + messages, + unread_count, + .. + } => { + if !ui.dm_conversations.iter().any(|c| c.dm_id == dm_id) { + ui.dm_conversations.push(crate::ui::state::DmConversation { + dm_id: dm_id.clone(), + other_nickname, + unread_count, + e2ee_enabled: false, + e2ee_peer_public_key: None, + e2ee_shared_secret: None, + }); + } + ui.dm_messages + .insert(dm_id.clone(), messages.into_iter().map(into_msg).collect()); + ui.active_dm_id = Some(dm_id); + } + NetEvent::ChatMessage { + message_id, + channel_id, + sender_id, + content, + timestamp, + reply_to, + .. + } => { + ui.messages + .entry(channel_id) + .or_default() + .push(ChatMessage { + message_id, + sender_id, + content, + timestamp, + edited: false, + reactions: HashMap::new(), + reply_to, + }); + ui.scroll_bottom = true; + } + NetEvent::ChatHistory { + channel_id, + messages, + } => { + ui.messages + .insert(channel_id, messages.into_iter().map(into_msg).collect()); + ui.scroll_bottom = true; + } + NetEvent::Error { message, .. } => { + ui.status_msg = Some(format!("error: {message}")); + } + NetEvent::ReactionAdded { + channel_id, + message_id, + user_id, + emoji, + } => { + if let Some(msgs) = ui.messages.get_mut(&channel_id) + && let Some(msg) = msgs.iter_mut().find(|m| m.message_id == message_id) + { + msg.reactions.entry(emoji).or_default().push(user_id); + } + } + NetEvent::ReactionRemoved { + channel_id, + message_id, + user_id, + emoji, + } => { + if let Some(msgs) = ui.messages.get_mut(&channel_id) + && let Some(msg) = msgs.iter_mut().find(|m| m.message_id == message_id) + && let Some(users) = msg.reactions.get_mut(&emoji) + { + users.retain(|u| u != &user_id); + if users.is_empty() { + msg.reactions.remove(&emoji); + } + } + } + NetEvent::MessageEdited { + channel_id, + message_id, + content, + .. + } => { + if let Some(msgs) = ui.messages.get_mut(&channel_id) + && let Some(msg) = msgs.iter_mut().find(|m| m.message_id == message_id) + { + msg.content = content; + msg.edited = true; + } + } + NetEvent::MessageDeleted { + channel_id, + message_id, + } => { + if let Some(msgs) = ui.messages.get_mut(&channel_id) { + msgs.retain(|m| m.message_id != message_id); + } + } + NetEvent::DmMessage { + dm_id, + sender_id, + content, + timestamp, + .. + } => { + ui.dm_messages + .entry(dm_id.clone()) + .or_default() + .push(ChatMessage { + message_id: uuid::Uuid::new_v4().to_string(), + sender_id, + content, + timestamp, + edited: false, + reactions: HashMap::new(), + reply_to: None, + }); + if ui.active_dm_id.as_deref() != Some(&dm_id) + && let Some(c) = ui.dm_conversations.iter_mut().find(|c| c.dm_id == dm_id) + { + c.unread_count += 1; + } + } + NetEvent::DmHistory { dm_id, messages } => { + ui.dm_messages + .insert(dm_id, messages.into_iter().map(into_msg).collect()); + } + NetEvent::ReadReceiptBroadcast { + channel_id, + user_id, + last_read_message_id, + } => { + ui.read_receipts + .entry(channel_id) + .or_default() + .insert(user_id, last_read_message_id); + } + NetEvent::UserColor { user_id, color } => { + if let Some(c) = parse_hex_color(&color) { + ui.user_colors.insert(user_id, c); + } + } + _ => {} + } +} + +fn social_handle(ui: &mut UiState, event: NetEvent) { + match event { + NetEvent::GuildAuditLog { .. } => {} + NetEvent::GuildMemberList { .. } => {} + NetEvent::GuildRoleList { .. } => {} + NetEvent::BlockList { blocked } => { + ui.blocked_users = blocked; + } + NetEvent::BlockedUser { user_id } => { + if !ui.blocked_users.contains(&user_id) { + ui.blocked_users.push(user_id); + } + } + NetEvent::UnblockedUser { user_id } => { + ui.blocked_users.retain(|u| u != &user_id); + } + NetEvent::GuildList { guilds } => { + ui.guilds = guilds + .into_iter() + .map(|g| crate::ui::state::GuildState { + guild_id: g.guild_id, + name: g.name, + member_count: g.member_count, + }) + .collect(); + } + NetEvent::GuildCreated { + guild_id, + name, + .. + } => { + ui.guilds.push(crate::ui::state::GuildState { + guild_id: guild_id.clone(), + name, + member_count: 1, + }); + ui.active_guild_id = Some(guild_id); + } + NetEvent::GuildDeleted { guild_id } => { + ui.guilds.retain(|g| g.guild_id != guild_id); + if ui.active_guild_id.as_deref() == Some(&guild_id) { + ui.active_guild_id = None; + } + } + NetEvent::GuildMemberJoined { + guild_id, + user_id: _, + nickname: _, + } => { + if let Some(g) = ui.guilds.iter_mut().find(|g| g.guild_id == guild_id) { + g.member_count += 1; + } + } + NetEvent::GuildMemberLeft { + guild_id, + user_id: _, + } => { + if let Some(g) = ui.guilds.iter_mut().find(|g| g.guild_id == guild_id) { + g.member_count = g.member_count.saturating_sub(1); + } + } + NetEvent::GuildMemberKicked { + guild_id, + user_id: _, + } => { + if let Some(g) = ui.guilds.iter_mut().find(|g| g.guild_id == guild_id) { + g.member_count = g.member_count.saturating_sub(1); + } + } + NetEvent::InviteCreated { .. } => {} + NetEvent::InviteAccepted { + guild_id, + guild_name, + } if !ui.guilds.iter().any(|g| g.guild_id == guild_id) => { + ui.guilds.push(crate::ui::state::GuildState { + guild_id, + name: guild_name, + member_count: 1, + }); + } + NetEvent::InviteDeleted { .. } => {} + NetEvent::RoleCreated { .. } => {} + NetEvent::RoleDeleted { .. } => {} + NetEvent::FriendList { friends } => { + ui.friends = friends + .into_iter() + .map(|f| crate::ui::state::FriendState { + user_id: f.user_id, + nickname: f.nickname, + }) + .collect(); + } + NetEvent::FriendRequested { user_id, nickname } => { + ui.pending_friend_requests + .push(crate::ui::state::FriendState { + user_id, + nickname, + }); + } + NetEvent::FriendAccepted { user_id, nickname } => { + ui.pending_friend_requests.retain(|f| f.user_id != user_id); + if !ui.friends.iter().any(|f| f.user_id == user_id) { + ui.friends.push(crate::ui::state::FriendState { + user_id, + nickname, + }); + } + } + NetEvent::FriendRemoved { user_id } => { + ui.friends.retain(|f| f.user_id != user_id); + ui.pending_friend_requests.retain(|f| f.user_id != user_id); + } + _ => {} + } +} + +fn status_handle(ui: &mut UiState, _net: &NetHandle, event: NetEvent) { + match event { + NetEvent::LatencyUpdate { rtt_ms } => { + ui.rtt_ms = Some(rtt_ms); + } + NetEvent::TypingStart { + user_id, + nickname, + channel_id, + } => { + let entry = ui.typing_users.entry(channel_id).or_default(); + entry.retain(|(u, _)| u != &user_id); + entry.push((nickname, std::time::Instant::now())); + } + NetEvent::PresenceSync { presences } => { + for p in presences { + ui.presences.insert(p.user_id.clone(), p); + } + } + NetEvent::PresenceUpdated { + user_id, + status, + custom_status, + activity, + } => { + ui.presences.insert( + user_id.clone(), + PresenceInfo { + user_id, + status, + custom_status, + activity, + activity_text: None, + }, + ); + } + _ => {} + } +} + +pub fn pipeline_config(channel_id: &str, state: &AppState) -> PipelineConfig { + PipelineConfig { + channel_key: vnox_client::net::voice::channel_key(channel_id), + mic_device_index: state.ui.mic_device, + output_device_index: state.ui.output_device, + voice_bitrate_kbps: state.ui.voice_bitrate, + packet_interval: state.ui.packet_interval, + } +} + +pub fn start_voice(state: &mut AppState, channel_id: &str) { + let cfg = pipeline_config(channel_id, state); + if state.voice_channel.as_deref() == Some(channel_id) + && state.audio.is_some() + && state.last_pipeline_config.as_ref() == Some(&cfg) + { + return; + } + state.audio = None; + match vnox_client::audio::start(cfg.clone()) { + Ok(pipeline) => { + tracing::info!("voice active on channel {channel_id}"); + state.audio = Some(pipeline); + state.voice_channel = Some(channel_id.to_string()); + state.last_pipeline_config = Some(cfg); + state.ui.voice = VoiceUiState::Active { + channel: channel_id.to_string(), + }; + if state.ui.voice_joined_at.is_none() { + state.ui.voice_joined_at = Some(std::time::Instant::now()); + } + state.ui.status_msg = None; + } + Err(e) => { + tracing::warn!("audio start failed: {e}"); + state.voice_channel = None; + state.last_pipeline_config = None; + state.ui.voice = VoiceUiState::Error(e.to_string()); + state.ui.status_msg = Some(format!("microphone error: {e}")); + } + } +} + +pub fn stop_voice(state: &mut AppState) { + state.audio = None; + state.voice_channel = None; + state.last_pipeline_config = None; + state.ui.voice = VoiceUiState::Off; + state.ui.voice_joined_at = None; +} + +pub fn sync_voice_audio(state: &mut AppState) { + if !matches!(state.ui.conn, ConnState::Connected { .. }) { + stop_voice(state); + return; + } + let active_voice = state + .ui + .active_channel + .as_ref() + .and_then(|id| state.ui.channels.iter().find(|c| &c.id == id)) + .filter(|c| c.kind == "voice") + .map(|c| c.id.clone()); + match active_voice { + Some(id) => start_voice(state, &id), + None => stop_voice(state), + } +} + +pub fn flush_encoded_voice(state: &mut AppState) { + let Some(pipeline) = state.audio.as_ref() else { + return; + }; + while let Some(frame) = pipeline.poll_encoded() { + let net = state.net.clone(); + tokio::spawn(async move { + net.send(NetCommand::SendVoice { + channel_id: frame.channel_id, + voice_seq: frame.voice_seq, + timestamp: frame.timestamp, + opus_data: frame.data, + }) + .await; + }); + } +}