diff --git a/Cargo.lock b/Cargo.lock index adfe6cb..8c1a448 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -4,9 +4,9 @@ version = 4 [[package]] name = "ab_glyph" -version = "0.2.31" +version = "0.2.32" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e074464580a518d16a7126262fffaaa47af89d4099d4cb403f8ed938ba12ee7d" +checksum = "01c0457472c38ea5bd1c3b5ada5e368271cb550be7a4ca4a0b4634e9913f6cc2" dependencies = [ "ab_glyph_rasterizer", "owned_ttf_parser", @@ -18,6 +18,96 @@ version = "0.1.10" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "366ffbaa4442f4684d91e2cd7c5ea7c4ed8add41959a31447066e279e432b618" +[[package]] +name = "accesskit" +version = "0.21.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cf203f9d3bd8f29f98833d1fbef628df18f759248a547e7e01cfbf63cda36a99" + +[[package]] +name = "accesskit_atspi_common" +version = "0.14.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "29f73a9b855b6f4af4962a94553ef0c092b80cf5e17038724d5e30945d036f69" +dependencies = [ + "accesskit", + "accesskit_consumer", + "atspi-common", + "serde", + "thiserror 1.0.69", + "zvariant", +] + +[[package]] +name = "accesskit_consumer" +version = "0.30.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bdd06f5fea9819250fffd4debf926709f3593ac22f8c1541a2573e5ee0ca01cd" +dependencies = [ + "accesskit", + "hashbrown 0.15.5", +] + +[[package]] +name = "accesskit_macos" +version = "0.22.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "93fbaf15815f39084e0cb24950c232f0e3634702c2dfbf182ae3b4919a4a1d45" +dependencies = [ + "accesskit", + "accesskit_consumer", + "hashbrown 0.15.5", + "objc2 0.5.2", + "objc2-app-kit 0.2.2", + "objc2-foundation 0.2.2", +] + +[[package]] +name = "accesskit_unix" +version = "0.17.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "64926a930368d52d95422b822ede15014c04536cabaa2394f99567a1f4788dc6" +dependencies = [ + "accesskit", + "accesskit_atspi_common", + "async-channel", + "async-executor", + "async-task", + "atspi", + "futures-lite", + "futures-util", + "serde", + "zbus", +] + +[[package]] +name = "accesskit_windows" +version = "0.29.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "792991159fa9ba57459de59e12e918bb90c5346fea7d40ac1a11f8632b41e63a" +dependencies = [ + "accesskit", + "accesskit_consumer", + "hashbrown 0.15.5", + "static_assertions", + "windows 0.61.3", + "windows-core 0.61.2", +] + +[[package]] +name = "accesskit_winit" +version = "0.29.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cd9db0ea66997e3f4eae4a5f2c6b6486cf206642639ee629dbbb860ace1dec87" +dependencies = [ + "accesskit", + "accesskit_macos", + "accesskit_unix", + "accesskit_windows", + "raw-window-handle", + "winit", +] + [[package]] name = "ahash" version = "0.8.12" @@ -184,9 +274,9 @@ dependencies = [ [[package]] name = "async-fs" -version = "2.1.3" +version = "2.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "09f7e37c0ed80b2a977691c47dae8625cfb21e205827106c64f7c588766b2e50" +checksum = "8034a681df4aed8b8edbd7fbe472401ecf009251c8b40556b304567052e294c5" dependencies = [ "async-lock", "blocking", @@ -195,11 +285,11 @@ dependencies = [ [[package]] name = "async-io" -version = "2.5.0" +version = "2.6.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "19634d6336019ef220f09fd31168ce5c184b295cbf80345437cc36094ef223ca" +checksum = "456b8a8feb6f42d237746d4b3e9a178494627745c3c56c6ea55d92ba50d026fc" dependencies = [ - "async-lock", + "autocfg", "cfg-if", "concurrent-queue", "futures-io", @@ -208,7 +298,7 @@ dependencies = [ "polling", "rustix 1.1.2", "slab", - "windows-sys 0.60.2", + "windows-sys 0.61.2", ] [[package]] @@ -235,9 +325,9 @@ dependencies = [ [[package]] name = "async-process" -version = "2.4.0" +version = "2.5.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "65daa13722ad51e6ab1a1b9c01299142bc75135b337923cfa10e79bbbd669f00" +checksum = "fc50921ec0055cdd8a16de48773bfeec5c972598674347252c0399676be7da75" dependencies = [ "async-channel", "async-io", @@ -264,9 +354,9 @@ dependencies = [ [[package]] name = "async-signal" -version = "0.2.12" +version = "0.2.13" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f567af260ef69e1d52c2b560ce0ea230763e6fbb9214a85d768760a920e3e3c1" +checksum = "43c070bbf59cd3570b6b2dd54cd772527c7c3620fce8be898406dd3ed6adc64c" dependencies = [ "async-io", "async-lock", @@ -277,7 +367,7 @@ dependencies = [ "rustix 1.1.2", "signal-hook-registry", "slab", - "windows-sys 0.60.2", + "windows-sys 0.61.2", ] [[package]] @@ -303,6 +393,56 @@ version = "1.1.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "1505bd5d3d116872e7271a6d4e16d81d0c8570876c8de68093a09ac269d8aac0" +[[package]] +name = "atspi" +version = "0.25.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c83247582e7508838caf5f316c00791eee0e15c0bf743e6880585b867e16815c" +dependencies = [ + "atspi-common", + "atspi-connection", + "atspi-proxies", +] + +[[package]] +name = "atspi-common" +version = "0.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "33dfc05e7cdf90988a197803bf24f5788f94f7c94a69efa95683e8ffe76cfdfb" +dependencies = [ + "enumflags2", + "serde", + "static_assertions", + "zbus", + "zbus-lockstep", + "zbus-lockstep-macros", + "zbus_names", + "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" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d2eebcb9e7e76f26d0bcfd6f0295e1cd1e6f33bedbc5698a971db8dc43d7751c" +dependencies = [ + "atspi-common", + "serde", + "zbus", +] + [[package]] name = "autocfg" version = "1.5.0" @@ -335,9 +475,6 @@ name = "bitflags" version = "2.9.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "2261d10cca569e4643e526d8dc2e62e433cc8aba21ab764233731f8d369bf394" -dependencies = [ - "serde", -] [[package]] name = "block" @@ -356,11 +493,11 @@ dependencies = [ [[package]] name = "block2" -version = "0.6.1" +version = "0.6.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "340d2f0bdb2a43c1d3cd40513185b2bd7def0aa1052f956455114bc98f82dcf2" +checksum = "cdeb9d870516001442e364c5220d3574d2da8dc765554b4a617230d33fa58ef5" dependencies = [ - "objc2 0.6.2", + "objc2 0.6.3", ] [[package]] @@ -384,18 +521,18 @@ checksum = "46c5e41b57b8bba42a04676d81cb89e9ee8e859a1a66f80a5a72e1cb76b34d43" [[package]] name = "bytemuck" -version = "1.23.2" +version = "1.24.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3995eaeebcdf32f91f980d360f78732ddc061097ab4e39991ae7a6ace9194677" +checksum = "1fbdf580320f38b612e485521afda1ee26d10cc9884efaaa750d383e13e3c5f4" dependencies = [ "bytemuck_derive", ] [[package]] name = "bytemuck_derive" -version = "1.10.1" +version = "1.10.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4f154e572231cb6ba2bd1176980827e3d5dc04cc183a75dea38109fbdd672d29" +checksum = "f9abbd1bc6865053c427f7198e6af43bfdedc55ab791faed4fbd361d789575ff" dependencies = [ "proc-macro2", "quote", @@ -436,9 +573,9 @@ dependencies = [ [[package]] name = "cc" -version = "1.2.36" +version = "1.2.41" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5252b3d2648e5eedbc1a6f501e3c795e07025c1e93bbf8bbdd6eef7f447a6d54" +checksum = "ac9fe6cdbb24b6ade63616c0a0688e45bb56732262c158df3c0c4bea4ca47cb7" dependencies = [ "find-msvc-tools", "jobserver", @@ -764,9 +901,9 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "89a09f22a6c6069a18470eb92d2298acf25463f14256d24778e1230d789a2aec" dependencies = [ "bitflags 2.9.4", - "block2 0.6.1", + "block2 0.6.2", "libc", - "objc2 0.6.2", + "objc2 0.6.3", ] [[package]] @@ -898,7 +1035,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "39cab71617ae0d63f51a36d69f866391735b51691dbda63cf6f96d042b63efeb" dependencies = [ "libc", - "windows-sys 0.61.0", + "windows-sys 0.61.2", ] [[package]] @@ -936,9 +1073,9 @@ checksum = "37909eebbb50d72f9059c3b6d82c0463f2ff062c9e95845c43a6c9c0355411be" [[package]] name = "find-msvc-tools" -version = "0.1.1" +version = "0.1.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7fd99930f64d146689264c637b5af2f0233a933bef0d8570e2526bf9e083192d" +checksum = "52051878f80a721bb68ebfbc930e07b65ba72f2da88968ea5c06fd6ca3d3a127" [[package]] name = "foldhash" @@ -946,6 +1083,12 @@ version = "0.1.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d9c4f5dac5e15c24eb999c26181a6ca40b39fe946cbe4c263c7209467bc83af2" +[[package]] +name = "foldhash" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "77ce24cb58228fbb8aa041425bb1050850ac19177686ea6e0f41a70416f56fdb" + [[package]] name = "font8x8" version = "0.3.1" @@ -1148,7 +1291,7 @@ checksum = "b89c83349105e3732062a895becfc71a8f921bb71ecbbdd8ff99263e3b53a0ca" dependencies = [ "bitflags 2.9.4", "gpu-descriptor-types", - "hashbrown", + "hashbrown 0.15.5", ] [[package]] @@ -1162,13 +1305,14 @@ dependencies = [ [[package]] name = "half" -version = "2.6.0" +version = "2.7.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "459196ed295495a68f7d7fe1d84f6c4b7ff0e21fe3017b2f283c6fac3ad803c9" +checksum = "e54c115d4f30f52c67202f079c5f9d8b49db4691f460fdb0b4c2e838261b2ba5" dependencies = [ "cfg-if", "crunchy", "num-traits", + "zerocopy", ] [[package]] @@ -1177,7 +1321,16 @@ version = "0.15.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9229cfe53dfd69f0609a49f65461bd93001ea1ef889cd5529dd176593f5338a1" dependencies = [ - "foldhash", + "foldhash 0.1.5", +] + +[[package]] +name = "hashbrown" +version = "0.16.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5419bdc4f6a9207fbeba6d11b604d481addf78ecd10c11ad51e76c2f6482748d" +dependencies = [ + "foldhash 0.2.0", ] [[package]] @@ -1307,12 +1460,12 @@ dependencies = [ [[package]] name = "indexmap" -version = "2.11.1" +version = "2.11.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "206a8042aec68fa4a62e8d3f7aa4ceb508177d9324faf261e1959e495b7a1921" +checksum = "4b0f83760fb341a774ed326568e19f5a863af4a952def8c39f9ab92fd95b88e5" dependencies = [ "equivalent", - "hashbrown", + "hashbrown 0.16.0", ] [[package]] @@ -1349,9 +1502,9 @@ dependencies = [ [[package]] name = "js-sys" -version = "0.3.78" +version = "0.3.81" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0c0b063578492ceec17683ef2f8c5e89121fbd0b172cbc280635ab7567db2738" +checksum = "ec48937a97411dcb524a265206ccd4c90bb711fca92b2792c407f268825b9305" dependencies = [ "once_cell", "wasm-bindgen", @@ -1382,18 +1535,18 @@ checksum = "bbd2bcb4c963f2ddae06a2efc7e9f3591312473c50c6685e1f298068316e66fe" [[package]] name = "libc" -version = "0.2.175" +version = "0.2.177" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6a82ae493e598baaea5209805c49bbf2ea7de956d50d7da0da1164f9c6d28543" +checksum = "2874a2af47a2325c2001a6e6fad9b16a53b802102b528163885171cf92b15976" [[package]] name = "libloading" -version = "0.8.8" +version = "0.8.9" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "07033963ba89ebaf1584d767badaa2e8fcec21aedea6b8c0346d487d49c28667" +checksum = "d7c4b02199fee7c5d21a5ae7d8cfa79a6ef5bb2fc834d6e9058e89c825efdc55" dependencies = [ "cfg-if", - "windows-targets 0.53.3", + "windows-link 0.2.1", ] [[package]] @@ -1404,13 +1557,13 @@ checksum = "f9fbbcab51052fe104eb5e5d351cf728d30a5be1fe14d9be8a3b097481fb97de" [[package]] name = "libredox" -version = "0.1.9" +version = "0.1.10" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "391290121bad3d37fbddad76d8f5d1c1c314cfc646d143d7e07a3086ddff0ce3" +checksum = "416f7e718bdb06000964960ffa43b4335ad4012ae8b99060261aa4a8088d5ccb" dependencies = [ "bitflags 2.9.4", "libc", - "redox_syscall 0.5.17", + "redox_syscall 0.5.18", ] [[package]] @@ -1445,11 +1598,10 @@ checksum = "f5e54036fe321fd421e10d732f155734c4e4afd610dd556d9a82833ab3ee0bed" [[package]] name = "lock_api" -version = "0.4.13" +version = "0.4.14" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "96936507f153605bddfcda068dd804796c84324ed2510809e5b2a624c81da765" +checksum = "224399e74b87b5f3557511d98dff8b14089b3dadafcab6bb93eab67d3aace965" dependencies = [ - "autocfg", "scopeguard", ] @@ -1479,9 +1631,9 @@ dependencies = [ [[package]] name = "memchr" -version = "2.7.5" +version = "2.7.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "32a282da65faaf38286cf3be983213fcf1d2e2a58700e808f83f4ea9a4804bc0" +checksum = "f52b00d39961fc5b2736ea853c9cc86238e165017a493d1d5c8eac6bdc4cc273" [[package]] name = "memmap2" @@ -1518,9 +1670,9 @@ dependencies = [ [[package]] name = "naga" -version = "26.0.0" +version = "27.0.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "916cbc7cb27db60be930a4e2da243cf4bc39569195f22fd8ee419cd31d5b662c" +checksum = "12b2e757b11b47345d44e7760e45458339bc490463d9548cd8651c53ae523153" dependencies = [ "arrayvec", "bit-set", @@ -1529,7 +1681,7 @@ dependencies = [ "cfg_aliases", "codespan-reporting", "half", - "hashbrown", + "hashbrown 0.16.0", "hexf-parse", "indexmap", "libm", @@ -1538,7 +1690,7 @@ dependencies = [ "once_cell", "rustc-hash", "spirv", - "thiserror 2.0.16", + "thiserror 2.0.17", "unicode-ident", ] @@ -1665,9 +1817,9 @@ dependencies = [ [[package]] name = "objc2" -version = "0.6.2" +version = "0.6.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "561f357ba7f3a2a61563a186a163d0a3a5247e1089524a3981d49adb775078bc" +checksum = "b7c2599ce0ec54857b29ce62166b0ed9b4f6f1a70ccc9a71165b6154caca8c05" dependencies = [ "objc2-encode", ] @@ -1690,29 +1842,29 @@ dependencies = [ [[package]] name = "objc2-app-kit" -version = "0.3.1" +version = "0.3.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e6f29f568bec459b0ddff777cec4fe3fd8666d82d5a40ebd0ff7e66134f89bcc" +checksum = "d49e936b501e5c5bf01fda3a9452ff86dc3ea98ad5f283e1455153142d97518c" dependencies = [ "bitflags 2.9.4", - "block2 0.6.1", - "objc2 0.6.2", - "objc2-foundation 0.3.1", + "block2 0.6.2", + "objc2 0.6.3", + "objc2-foundation 0.3.2", ] [[package]] name = "objc2-audio-toolbox" -version = "0.3.1" +version = "0.3.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "10cbe18d879e20a4aea544f8befe38bcf52255eb63d3f23eca2842f3319e4c07" +checksum = "6948501a91121d6399b79abaa33a8aa4ea7857fe019f341b8c23ad6e81b79b08" dependencies = [ "bitflags 2.9.4", "libc", - "objc2 0.6.2", + "objc2 0.6.3", "objc2-core-audio", "objc2-core-audio-types", "objc2-core-foundation", - "objc2-foundation 0.3.1", + "objc2-foundation 0.3.2", ] [[package]] @@ -1741,24 +1893,24 @@ dependencies = [ [[package]] name = "objc2-core-audio" -version = "0.3.1" +version = "0.3.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ca44961e888e19313b808f23497073e3f6b3c22bb485056674c8b49f3b025c82" +checksum = "e1eebcea8b0dbff5f7c8504f3107c68fc061a3eb44932051c8cf8a68d969c3b2" dependencies = [ "dispatch2", - "objc2 0.6.2", + "objc2 0.6.3", "objc2-core-audio-types", "objc2-core-foundation", ] [[package]] name = "objc2-core-audio-types" -version = "0.3.1" +version = "0.3.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c0f1cc99bb07ad2ddb6527ddf83db6a15271bb036b3eb94b801cd44fdc666ee1" +checksum = "5a89f2ec274a0cf4a32642b2991e8b351a404d290da87bb6a9a9d8632490bd1c" dependencies = [ "bitflags 2.9.4", - "objc2 0.6.2", + "objc2 0.6.3", ] [[package]] @@ -1775,13 +1927,13 @@ dependencies = [ [[package]] name = "objc2-core-foundation" -version = "0.3.1" +version = "0.3.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1c10c2894a6fed806ade6027bcd50662746363a9589d3ec9d9bef30a4e4bc166" +checksum = "2a180dd8642fa45cdb7dd721cd4c11b1cadd4929ce112ebd8b9f5803cc79d536" dependencies = [ "bitflags 2.9.4", "dispatch2", - "objc2 0.6.2", + "objc2 0.6.3", ] [[package]] @@ -1829,12 +1981,12 @@ dependencies = [ [[package]] name = "objc2-foundation" -version = "0.3.1" +version = "0.3.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "900831247d2fe1a09a683278e5384cfb8c80c79fe6b166f9d14bfdde0ea1b03c" +checksum = "e3e0adef53c21f888deb4fa59fc59f7eb17404926ee8a6f59f5df0fd7f9f3272" dependencies = [ "bitflags 2.9.4", - "objc2 0.6.2", + "objc2 0.6.3", "objc2-core-foundation", ] @@ -1981,9 +2133,9 @@ checksum = "f38d5652c16fde515bb1ecef450ab0f6a219d619a7274976324d5e377f7dceba" [[package]] name = "parking_lot" -version = "0.12.4" +version = "0.12.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "70d58bf43669b5795d1576d0641cfb6fbb2057bf629506267a92807158584a13" +checksum = "93857453250e3077bd71ff98b6a65ea6621a19bb0f559a85248955ac12c45a1a" dependencies = [ "lock_api", "parking_lot_core", @@ -1991,15 +2143,15 @@ dependencies = [ [[package]] name = "parking_lot_core" -version = "0.9.11" +version = "0.9.12" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "bc838d2a56b5b1a6c25f55575dfc605fabb63bb2365f6c2353ef9159aa69e4a5" +checksum = "2621685985a2ebf1c516881c026032ac7deafcda1a2c9b7850dc81e3dfcb64c1" dependencies = [ "cfg-if", "libc", - "redox_syscall 0.5.17", + "redox_syscall 0.5.18", "smallvec", - "windows-targets 0.52.6", + "windows-link 0.2.1", ] [[package]] @@ -2065,16 +2217,16 @@ checksum = "7edddbd0b52d732b21ad9a5fab5c704c14cd949e5e9a1ec5929a24fded1b904c" [[package]] name = "polling" -version = "3.10.0" +version = "3.11.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b5bd19146350fe804f7cb2669c851c03d69da628803dab0d98018142aaa5d829" +checksum = "5d0e4f59085d47d8241c88ead0f274e8a0cb551f3625263c05eb8dd897c34218" dependencies = [ "cfg-if", "concurrent-queue", "hermit-abi", "pin-project-lite", "rustix 1.1.2", - "windows-sys 0.60.2", + "windows-sys 0.61.2", ] [[package]] @@ -2124,9 +2276,9 @@ checksum = "e8cf8e6a8aa66ce33f63993ffc4ea4271eb5b0530a9002db8455ea6050c77bfa" [[package]] name = "proc-macro-crate" -version = "3.3.0" +version = "3.4.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "edce586971a4dfaa28950c6f18ed55e0406c1ab88bbce2c6f6293a7aaba73d35" +checksum = "219cb19e96be00ab2e37d6e299658a0cfa83e52429179969b0f0121b4ac46983" dependencies = [ "toml_edit", ] @@ -2146,6 +2298,16 @@ version = "1.0.17" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "3eb8486b569e12e2c32ad3e204dbaba5e4b5b216e9367044f25f1dba42341773" +[[package]] +name = "quick-xml" +version = "0.36.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f7649a7b4df05aed9ea7ec6f628c67c9953a43869b8bc50929569b2999d443fe" +dependencies = [ + "memchr", + "serde", +] + [[package]] name = "quick-xml" version = "0.37.5" @@ -2157,9 +2319,9 @@ dependencies = [ [[package]] name = "quote" -version = "1.0.40" +version = "1.0.41" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1885c039570dc00dcb4ff087a89e185fd56bae234ddc7f056a945bf36467248d" +checksum = "ce25767e7b499d1b604768e7cde645d14cc8584231ea6b295e9c9eb22c02e1d1" dependencies = [ "proc-macro2", ] @@ -2222,9 +2384,9 @@ dependencies = [ [[package]] name = "redox_syscall" -version = "0.5.17" +version = "0.5.18" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5407465600fb0548f1442edf71dd20683c6ed326200ace4b1ef0763521bb3b77" +checksum = "ed2bf2547551a7053d6fdfafda3f938979645c44812fbfcda098faae3f1a362d" dependencies = [ "bitflags 2.9.4", ] @@ -2242,14 +2404,14 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ef2bee61e6cffa4635c72d7d81a84294e28f0930db0ddcb0f66d10244674ebed" dependencies = [ "ashpd", - "block2 0.6.1", + "block2 0.6.2", "dispatch2", "js-sys", "log", - "objc2 0.6.2", - "objc2-app-kit 0.3.1", + "objc2 0.6.3", + "objc2-app-kit 0.3.2", "objc2-core-foundation", - "objc2-foundation 0.3.1", + "objc2-foundation 0.3.2", "pollster", "raw-window-handle", "urlencoding", @@ -2259,6 +2421,12 @@ dependencies = [ "windows-sys 0.59.0", ] +[[package]] +name = "rt-write-lock" +version = "0.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "79ce7f6e4b55e98feea9066f403ac0866c1d16d602d7522367151771d2800a15" + [[package]] name = "rtrb" version = "0.3.2" @@ -2324,7 +2492,7 @@ dependencies = [ "errno", "libc", "linux-raw-sys 0.11.0", - "windows-sys 0.61.0", + "windows-sys 0.61.2", ] [[package]] @@ -2369,18 +2537,28 @@ dependencies = [ [[package]] name = "serde" -version = "1.0.219" +version = "1.0.228" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9a8e94ea7f378bd32cbbd37198a4a91436180c5bb472411e48b5ec2e2124ae9e" +dependencies = [ + "serde_core", + "serde_derive", +] + +[[package]] +name = "serde_core" +version = "1.0.228" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5f0e2c6ed6606019b4e29e69dbaba95b11854410e5347d525002456dbbb786b6" +checksum = "41d385c7d4ca58e59fc732af25c3983b67ac852c1a25000afe1175de458b67ad" dependencies = [ "serde_derive", ] [[package]] name = "serde_derive" -version = "1.0.219" +version = "1.0.228" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5b0276cf7f2c73365f7157c8123c21cd9a50fbbd844757af28ca1f5925fc2a00" +checksum = "d540f220d3187173da220f885ab66608367b6574e925011a9353e4badda91d79" dependencies = [ "proc-macro2", "quote", @@ -2415,9 +2593,9 @@ dependencies = [ [[package]] name = "simple-left-right" -version = "0.2.2" +version = "0.2.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8d14c606977d5f05c85d6c9d30da1d49567dd92c16e0bc693644659bcfbe8b05" +checksum = "a3a8aae2755857e854bf0aaa2024e240c39c75228789afe556f552a52dbb9d74" [[package]] name = "slab" @@ -2511,7 +2689,7 @@ dependencies = [ "objc2-foundation 0.2.2", "objc2-quartz-core", "raw-window-handle", - "redox_syscall 0.5.17", + "redox_syscall 0.5.18", "rustix 0.38.44", "tiny-xlib", "wasm-bindgen", @@ -2534,9 +2712,9 @@ dependencies = [ [[package]] name = "stable_deref_trait" -version = "1.2.0" +version = "1.2.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a8f112729512f8e442d81f95a8a7ddf2b7c6b8a1a6f509a95864142b30cab2d3" +checksum = "6ce2be8dc25455e1f91df71bfa12ad37d7af1092ae736f3a6cd0e37bc7810596" [[package]] name = "static_assertions" @@ -2707,15 +2885,15 @@ dependencies = [ [[package]] name = "tempfile" -version = "3.22.0" +version = "3.23.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "84fa4d11fadde498443cca10fd3ac23c951f0dc59e080e9f4b93d4df4e4eea53" +checksum = "2d31c77bdf42a745371d260a26ca7163f1e0924b64afa0b688e61b5a9fa02f16" dependencies = [ "fastrand", "getrandom", "once_cell", "rustix 1.1.2", - "windows-sys 0.61.0", + "windows-sys 0.61.2", ] [[package]] @@ -2738,11 +2916,11 @@ dependencies = [ [[package]] name = "thiserror" -version = "2.0.16" +version = "2.0.17" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3467d614147380f2e4e374161426ff399c91084acd2363eaf549172b3d5e60c0" +checksum = "f63587ca0f12b72a0600bcba1d40081f830876000bb46dd2337a3051618f4fc8" dependencies = [ - "thiserror-impl 2.0.16", + "thiserror-impl 2.0.17", ] [[package]] @@ -2758,9 +2936,9 @@ dependencies = [ [[package]] name = "thiserror-impl" -version = "2.0.16" +version = "2.0.17" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6c5e1be1c48b9172ee610da68fd9cd2770e7a4056cb3fc98710ee6906f0c7960" +checksum = "3ff15c8ecd7de3849db632e14d18d2571fa09dfc5ed93479bc4485c7a517c913" dependencies = [ "proc-macro2", "quote", @@ -2817,18 +2995,31 @@ dependencies = [ [[package]] name = "toml_datetime" -version = "0.6.11" +version = "0.7.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "22cddaf88f4fbc13c51aebbf5f8eceb5c7c5a9da2ac40a13519eb5b0a0e8f11c" +checksum = "f2cdb639ebbc97961c51720f858597f7f24c4fc295327923af55b74c3c724533" +dependencies = [ + "serde_core", +] [[package]] name = "toml_edit" -version = "0.22.27" +version = "0.23.7" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "41fe8c660ae4257887cf66394862d21dbca4a6ddd26f04a3560410406a2f819a" +checksum = "6485ef6d0d9b5d0ec17244ff7eb05310113c3f316f2d14200d4de56b3cb98f8d" dependencies = [ "indexmap", "toml_datetime", + "toml_parser", + "winnow", +] + +[[package]] +name = "toml_parser" +version = "1.0.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c0cbe268d35bdb4bb5a56a2de88d0ad0eb70af5384a99d648cd4b3d04039800e" +dependencies = [ "winnow", ] @@ -2836,6 +3027,8 @@ dependencies = [ name = "torque-tracker" version = "0.1.1" dependencies = [ + "accesskit", + "accesskit_winit", "ascii", "cpal", "font8x8", @@ -2845,22 +3038,20 @@ dependencies = [ "softbuffer", "symphonia", "torque-tracker-engine", - "triple_buffer", "wgpu", "winit", ] [[package]] name = "torque-tracker-engine" -version = "0.1.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "809ceef6db8d2bd9bf2a14b327efe8fd04d16827e3675a035f70d00503c2ae4f" +version = "0.2.0" +source = "git+https://github.com/luca3s/torque-tracker-engine.git?branch=dev#6769b8159285a0b3ba6139f22e484d66bd7a0b97" dependencies = [ "dasp", + "rt-write-lock", "rtrb", "rtsan-standalone", "simple-left-right", - "triple_buffer", ] [[package]] @@ -2894,15 +3085,6 @@ dependencies = [ "once_cell", ] -[[package]] -name = "triple_buffer" -version = "8.1.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "420466259f9fa5decc654c490b9ab538400e5420df8237f84ecbe20368bcf72b" -dependencies = [ - "crossbeam-utils", -] - [[package]] name = "ttf-parser" version = "0.25.1" @@ -2922,9 +3104,9 @@ dependencies = [ [[package]] name = "unicode-ident" -version = "1.0.18" +version = "1.0.19" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5a5f39404a5da50712a4c1eecf25e90dd62b613502b7e925fd4e4d19b5c96512" +checksum = "f63a545481291138910575129486daeaf8ac54aee4387fe7906919f7830c7d9d" [[package]] name = "unicode-segmentation" @@ -2934,9 +3116,9 @@ checksum = "f6ccf251212114b54433ec949fd6a7841275f9ada20dddd2f29e9ceea4501493" [[package]] name = "unicode-width" -version = "0.2.1" +version = "0.2.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4a1a07cc7db3810833284e8d372ccdc6da29741639ecc70c9ec107df0fa6154c" +checksum = "b4ac048d71ede7ee76d585517add45da530660ef4390e49b098733c6e897f254" [[package]] name = "url" @@ -2980,18 +3162,27 @@ dependencies = [ [[package]] name = "wasi" -version = "0.14.4+wasi-0.2.4" +version = "0.14.7+wasi-0.2.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "883478de20367e224c0090af9cf5f9fa85bed63a95c1abf3afc5c083ebc06e8c" +dependencies = [ + "wasip2", +] + +[[package]] +name = "wasip2" +version = "1.0.1+wasi-0.2.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "88a5f4a424faf49c3c2c344f166f0662341d470ea185e939657aaff130f0ec4a" +checksum = "0562428422c63773dad2c345a1882263bbf4d65cf3f42e90921f787ef5ad58e7" dependencies = [ "wit-bindgen", ] [[package]] name = "wasm-bindgen" -version = "0.2.101" +version = "0.2.104" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7e14915cadd45b529bb8d1f343c4ed0ac1de926144b746e2710f9cd05df6603b" +checksum = "c1da10c01ae9f1ae40cbfac0bac3b1e724b320abfcf52229f80b547c0d250e2d" dependencies = [ "cfg-if", "once_cell", @@ -3002,9 +3193,9 @@ dependencies = [ [[package]] name = "wasm-bindgen-backend" -version = "0.2.101" +version = "0.2.104" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e28d1ba982ca7923fd01448d5c30c6864d0a14109560296a162f80f305fb93bb" +checksum = "671c9a5a66f49d8a47345ab942e2cb93c7d1d0339065d4f8139c486121b43b19" dependencies = [ "bumpalo", "log", @@ -3016,9 +3207,9 @@ dependencies = [ [[package]] name = "wasm-bindgen-futures" -version = "0.4.51" +version = "0.4.54" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0ca85039a9b469b38336411d6d6ced91f3fc87109a2a27b0c197663f5144dffe" +checksum = "7e038d41e478cc73bae0ff9b36c60cff1c98b8f38f8d7e8061e79ee63608ac5c" dependencies = [ "cfg-if", "js-sys", @@ -3029,9 +3220,9 @@ dependencies = [ [[package]] name = "wasm-bindgen-macro" -version = "0.2.101" +version = "0.2.104" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7c3d463ae3eff775b0c45df9da45d68837702ac35af998361e2c84e7c5ec1b0d" +checksum = "7ca60477e4c59f5f2986c50191cd972e3a50d8a95603bc9434501cf156a9a119" dependencies = [ "quote", "wasm-bindgen-macro-support", @@ -3039,9 +3230,9 @@ dependencies = [ [[package]] name = "wasm-bindgen-macro-support" -version = "0.2.101" +version = "0.2.104" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7bb4ce89b08211f923caf51d527662b75bdc9c9c7aab40f86dcb9fb85ac552aa" +checksum = "9f07d2f20d4da7b26400c9f4a0511e6e0345b040694e8a75bd41d578fa4421d7" dependencies = [ "proc-macro2", "quote", @@ -3052,9 +3243,9 @@ dependencies = [ [[package]] name = "wasm-bindgen-shared" -version = "0.2.101" +version = "0.2.104" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f143854a3b13752c6950862c906306adb27c7e839f7414cec8fea35beab624c1" +checksum = "bad67dc8b2a1a6e5448428adec4c3e84c43e561d8c9ee8a9e5aabeb193ec41d1" dependencies = [ "unicode-ident", ] @@ -3152,7 +3343,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "54cb1e9dc49da91950bdfd8b848c49330536d9d1fb03d4bfec8cae50caa50ae3" dependencies = [ "proc-macro2", - "quick-xml", + "quick-xml 0.37.5", "quote", ] @@ -3170,9 +3361,9 @@ dependencies = [ [[package]] name = "web-sys" -version = "0.3.78" +version = "0.3.81" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "77e4b637749ff0d92b8fad63aa1f7cff3cbe125fd49c175cd6345e7272638b12" +checksum = "9367c417a924a74cae129e6a2ae3b47fabb1f8995595ab474029da749a8be120" dependencies = [ "js-sys", "wasm-bindgen", @@ -3190,16 +3381,16 @@ dependencies = [ [[package]] name = "wgpu" -version = "26.0.1" +version = "27.0.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "70b6ff82bbf6e9206828e1a3178e851f8c20f1c9028e74dd3a8090741ccd5798" +checksum = "bfe68bac7cde125de7a731c3400723cadaaf1703795ad3f4805f187459cd7a77" dependencies = [ "arrayvec", "bitflags 2.9.4", "cfg-if", "cfg_aliases", "document-features", - "hashbrown", + "hashbrown 0.16.0", "js-sys", "log", "naga", @@ -3219,17 +3410,18 @@ dependencies = [ [[package]] name = "wgpu-core" -version = "26.0.1" +version = "27.0.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d5f62f1053bd28c2268f42916f31588f81f64796e2ff91b81293515017ca8bd9" +checksum = "e3d654c0b6c6335edfca18c11bdaed964def641b8e9997d3a495a2ff4077c922" dependencies = [ "arrayvec", "bit-set", "bit-vec", "bitflags 2.9.4", + "bytemuck", "cfg_aliases", "document-features", - "hashbrown", + "hashbrown 0.16.0", "indexmap", "log", "naga", @@ -3240,7 +3432,7 @@ dependencies = [ "raw-window-handle", "rustc-hash", "smallvec", - "thiserror 2.0.16", + "thiserror 2.0.17", "wgpu-core-deps-apple", "wgpu-core-deps-emscripten", "wgpu-core-deps-windows-linux-android", @@ -3250,36 +3442,36 @@ dependencies = [ [[package]] name = "wgpu-core-deps-apple" -version = "26.0.0" +version = "27.0.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "18ae5fbde6a4cbebae38358aa73fcd6e0f15c6144b67ef5dc91ded0db125dbdf" +checksum = "0772ae958e9be0c729561d5e3fd9a19679bcdfb945b8b1a1969d9bfe8056d233" dependencies = [ "wgpu-hal", ] [[package]] name = "wgpu-core-deps-emscripten" -version = "26.0.0" +version = "27.0.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d7670e390f416006f746b4600fdd9136455e3627f5bd763abf9a65daa216dd2d" +checksum = "b06ac3444a95b0813ecfd81ddb2774b66220b264b3e2031152a4a29fda4da6b5" dependencies = [ "wgpu-hal", ] [[package]] name = "wgpu-core-deps-windows-linux-android" -version = "26.0.0" +version = "27.0.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "720a5cb9d12b3d337c15ff0e24d3e97ed11490ff3f7506e7f3d98c68fa5d6f14" +checksum = "71197027d61a71748e4120f05a9242b2ad142e3c01f8c1b47707945a879a03c3" dependencies = [ "wgpu-hal", ] [[package]] name = "wgpu-hal" -version = "26.0.4" +version = "27.0.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7df2c64ac282a91ad7662c90bc4a77d4a2135bc0b2a2da5a4d4e267afc034b9e" +checksum = "2618a2d6b8a5964ecc1ac32a5db56cb3b1e518725fcd773fd9a782e023453f2b" dependencies = [ "android_system_properties", "arrayvec", @@ -3296,7 +3488,7 @@ dependencies = [ "gpu-alloc", "gpu-allocator", "gpu-descriptor", - "hashbrown", + "hashbrown 0.16.0", "js-sys", "khronos-egl", "libc", @@ -3306,6 +3498,7 @@ dependencies = [ "naga", "ndk-sys", "objc", + "once_cell", "ordered-float", "parking_lot", "portable-atomic", @@ -3315,7 +3508,7 @@ dependencies = [ "raw-window-handle", "renderdoc-sys", "smallvec", - "thiserror 2.0.16", + "thiserror 2.0.17", "wasm-bindgen", "web-sys", "wgpu-types", @@ -3325,15 +3518,15 @@ dependencies = [ [[package]] name = "wgpu-types" -version = "26.0.0" +version = "27.0.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "eca7a8d8af57c18f57d393601a1fb159ace8b2328f1b6b5f80893f7d672c9ae2" +checksum = "afdcf84c395990db737f2dd91628706cb31e86d72e53482320d368e52b5da5eb" dependencies = [ "bitflags 2.9.4", "bytemuck", "js-sys", "log", - "thiserror 2.0.16", + "thiserror 2.0.17", "web-sys", ] @@ -3359,7 +3552,7 @@ version = "0.1.11" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "c2a7b1c03c876122aa43f3020e6c3c3ee5c05081c9a00739faf7503aeba10d22" dependencies = [ - "windows-sys 0.61.0", + "windows-sys 0.61.2", ] [[package]] @@ -3388,6 +3581,28 @@ dependencies = [ "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", + "windows-core 0.61.2", + "windows-future", + "windows-link 0.1.3", + "windows-numerics", +] + +[[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", +] + [[package]] name = "windows-core" version = "0.54.0" @@ -3404,13 +3619,37 @@ version = "0.58.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "6ba6d44ec8c2591c134257ce647b7ea6b20335bf6379a27dac5f1641fcf59f99" dependencies = [ - "windows-implement", - "windows-interface", + "windows-implement 0.58.0", + "windows-interface 0.58.0", "windows-result 0.2.0", - "windows-strings", + "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", +] + +[[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", +] + [[package]] name = "windows-implement" version = "0.58.0" @@ -3422,6 +3661,17 @@ dependencies = [ "syn", ] +[[package]] +name = "windows-implement" +version = "0.60.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "053e2e040ab57b9dc951b72c264860db7eb3b0200ba345b4e4c3b14f67855ddf" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + [[package]] name = "windows-interface" version = "0.58.0" @@ -3433,6 +3683,17 @@ dependencies = [ "syn", ] +[[package]] +name = "windows-interface" +version = "0.59.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3f316c4a2570ba26bbec722032c4099d8c8bc095efccdc15688708623367e358" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + [[package]] name = "windows-link" version = "0.1.3" @@ -3441,9 +3702,19 @@ 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 = "45e46c0661abb7180e7b9c281db115305d49ca1709ab8242adf09666d2173c65" +checksum = "9150af68066c4c5c07ddc0ce30421554771e528bde427614c61038bc2c92c2b1" +dependencies = [ + "windows-core 0.61.2", + "windows-link 0.1.3", +] [[package]] name = "windows-result" @@ -3463,6 +3734,15 @@ 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", +] + [[package]] name = "windows-strings" version = "0.1.0" @@ -3473,6 +3753,15 @@ dependencies = [ "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", +] + [[package]] name = "windows-sys" version = "0.45.0" @@ -3506,16 +3795,16 @@ version = "0.60.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "f2f500e4d28234f72040990ec9d39e3a6b950f9f22d3dba18416c35882612bcb" dependencies = [ - "windows-targets 0.53.3", + "windows-targets 0.53.5", ] [[package]] name = "windows-sys" -version = "0.61.0" +version = "0.61.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e201184e40b2ede64bc2ea34968b28e33622acdbbf37104f0e4a33f7abe657aa" +checksum = "ae137229bcbd6cdf0f7b80a31df61766145077ddf49416a728b02cb3921ff3fc" dependencies = [ - "windows-link 0.2.0", + "windows-link 0.2.1", ] [[package]] @@ -3551,19 +3840,28 @@ dependencies = [ [[package]] name = "windows-targets" -version = "0.53.3" +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 = "d5fe6031c4041849d7c496a8ded650796e7b6ecc19df1a431c1a363342e5dc91" +checksum = "b66463ad2e0ea3bbf808b7f1d371311c80e115c0b71d60efc142cafbcfb057a6" dependencies = [ "windows-link 0.1.3", - "windows_aarch64_gnullvm 0.53.0", - "windows_aarch64_msvc 0.53.0", - "windows_i686_gnu 0.53.0", - "windows_i686_gnullvm 0.53.0", - "windows_i686_msvc 0.53.0", - "windows_x86_64_gnu 0.53.0", - "windows_x86_64_gnullvm 0.53.0", - "windows_x86_64_msvc 0.53.0", ] [[package]] @@ -3580,9 +3878,9 @@ checksum = "32a4622180e7a0ec044bb555404c800bc9fd9ec262ec147edd5989ccd0c02cd3" [[package]] name = "windows_aarch64_gnullvm" -version = "0.53.0" +version = "0.53.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "86b8d5f90ddd19cb4a147a5fa63ca848db3df085e25fee3cc10b39b6eebae764" +checksum = "a9d8416fa8b42f5c947f8482c43e7d89e73a173cead56d044f6a56104a6d1b53" [[package]] name = "windows_aarch64_msvc" @@ -3598,9 +3896,9 @@ checksum = "09ec2a7bb152e2252b53fa7803150007879548bc709c039df7627cabbd05d469" [[package]] name = "windows_aarch64_msvc" -version = "0.53.0" +version = "0.53.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c7651a1f62a11b8cbd5e0d42526e55f2c99886c77e007179efff86c2b137e66c" +checksum = "b9d782e804c2f632e395708e99a94275910eb9100b2114651e04744e9b125006" [[package]] name = "windows_i686_gnu" @@ -3616,9 +3914,9 @@ checksum = "8e9b5ad5ab802e97eb8e295ac6720e509ee4c243f69d781394014ebfe8bbfa0b" [[package]] name = "windows_i686_gnu" -version = "0.53.0" +version = "0.53.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c1dc67659d35f387f5f6c479dc4e28f1d4bb90ddd1a5d3da2e5d97b42d6272c3" +checksum = "960e6da069d81e09becb0ca57a65220ddff016ff2d6af6a223cf372a506593a3" [[package]] name = "windows_i686_gnullvm" @@ -3628,9 +3926,9 @@ checksum = "0eee52d38c090b3caa76c563b86c3a4bd71ef1a819287c19d586d7334ae8ed66" [[package]] name = "windows_i686_gnullvm" -version = "0.53.0" +version = "0.53.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9ce6ccbdedbf6d6354471319e781c0dfef054c81fbc7cf83f338a4296c0cae11" +checksum = "fa7359d10048f68ab8b09fa71c3daccfb0e9b559aed648a8f95469c27057180c" [[package]] name = "windows_i686_msvc" @@ -3646,9 +3944,9 @@ checksum = "240948bc05c5e7c6dabba28bf89d89ffce3e303022809e73deaefe4f6ec56c66" [[package]] name = "windows_i686_msvc" -version = "0.53.0" +version = "0.53.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "581fee95406bb13382d2f65cd4a908ca7b1e4c2f1917f143ba16efe98a589b5d" +checksum = "1e7ac75179f18232fe9c285163565a57ef8d3c89254a30685b57d83a38d326c2" [[package]] name = "windows_x86_64_gnu" @@ -3664,9 +3962,9 @@ checksum = "147a5c80aabfbf0c7d901cb5895d1de30ef2907eb21fbbab29ca94c5b08b1a78" [[package]] name = "windows_x86_64_gnu" -version = "0.53.0" +version = "0.53.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2e55b5ac9ea33f2fc1716d1742db15574fd6fc8dadc51caab1c16a3d3b4190ba" +checksum = "9c3842cdd74a865a8066ab39c8a7a473c0778a3f29370b5fd6b4b9aa7df4a499" [[package]] name = "windows_x86_64_gnullvm" @@ -3682,9 +3980,9 @@ checksum = "24d5b23dc417412679681396f2b49f3de8c1473deb516bd34410872eff51ed0d" [[package]] name = "windows_x86_64_gnullvm" -version = "0.53.0" +version = "0.53.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0a6e035dd0599267ce1ee132e51c27dd29437f63325753051e71dd9e42406c57" +checksum = "0ffa179e2d07eee8ad8f57493436566c7cc30ac536a3379fdf008f47f6bb7ae1" [[package]] name = "windows_x86_64_msvc" @@ -3700,9 +3998,9 @@ checksum = "589f6da84c646204747d1270a2a5661ea66ed1cced2631d546fdfb155959f9ec" [[package]] name = "windows_x86_64_msvc" -version = "0.53.0" +version = "0.53.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "271414315aff87387382ec3d271b52d7ae78726f5d44ac98b4f4030c91880486" +checksum = "d6bbff5f0aada427a1e5a6da5f1f98158182f26556f345ac9e04d36d0ebed650" [[package]] name = "winit" @@ -3767,9 +4065,9 @@ dependencies = [ [[package]] name = "wit-bindgen" -version = "0.45.1" +version = "0.46.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5c573471f125075647d03df72e026074b7203790d41351cd6edc96f46bcccd36" +checksum = "f17a85883d4e6d00e8a97c586de764dabcc06133f7f1d55dce5cdc070ad7fe59" [[package]] name = "writeable" @@ -3897,6 +4195,30 @@ dependencies = [ "zvariant", ] +[[package]] +name = "zbus-lockstep" +version = "0.5.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "29e96e38ded30eeab90b6ba88cb888d70aef4e7489b6cd212c5e5b5ec38045b6" +dependencies = [ + "zbus_xml", + "zvariant", +] + +[[package]] +name = "zbus-lockstep-macros" +version = "0.5.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dc6821851fa840b708b4cbbaf6241868cabc85a2dc22f426361b0292bfc0b836" +dependencies = [ + "proc-macro2", + "quote", + "syn", + "zbus-lockstep", + "zbus_xml", + "zvariant", +] + [[package]] name = "zbus_macros" version = "5.11.0" @@ -3924,6 +4246,19 @@ dependencies = [ "zvariant", ] +[[package]] +name = "zbus_xml" +version = "5.0.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "589e9a02bfafb9754bb2340a9e3b38f389772684c63d9637e76b1870377bec29" +dependencies = [ + "quick-xml 0.36.2", + "serde", + "static_assertions", + "zbus_names", + "zvariant", +] + [[package]] name = "zerocopy" version = "0.8.27" diff --git a/Cargo.toml b/Cargo.toml index 94d3b25..4eb4af3 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -11,26 +11,43 @@ categories = ["multimedia::audio"] # See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html [dependencies] +# my font font8x8 = "0.3.1" -wgpu = { version = "26.0.0", optional = true } +wgpu = { version = "27.0.0", optional = true } +# ascii strings ascii = "1.1.0" winit = "0.30.11" -torque-tracker-engine = "0.1.0" +# torque-tracker-engine = "0.1.0" +# torque-tracker-engine = { git = "https://tangled.org/@inkreas.ing/torque-tracker-engine.git", branch = "dev" } +torque-tracker-engine = { git = "https://github.com/luca3s/torque-tracker-engine.git", branch = "dev" } smol = "2.0.2" +# macro shit paste = "1.0.15" +# audio output cpal = "0.16.0" -triple_buffer = "8.1.1" softbuffer = { version="0.4.6", optional = true } +# cross platform file dialogs rfd = "0.15.4" +# audio file loading symphonia = "0.5.4" +accesskit_winit = { version = "0.29.1", optional = true } +accesskit = { version = "0.21.0", optional = true } [features] -# needs to be one, but not both -# less artifacts than software scaling. also probably faster +# at least one of {gpu/soft}_scaling needs to be enabled. If both are active gpu_scaling is prefered and +# softwave scaling is only used if the gpu initialisation fails. + +# gpu_scaling: less artifacts than software scaling. also probably faster gpu_scaling = ["dep:wgpu"] soft_scaling = ["dep:softbuffer"] -default = ["gpu_scaling"] +# accessability isn't an optional feature of course, but i want to make this progream compatible with embedded at some point. +# To not make this harder than necessary it is done like this +accesskit = ["dep:accesskit_winit", "dep:accesskit"] + +default = ["soft_scaling", "gpu_scaling", "accesskit"] [lints.clippy] uninlined_format_args = "allow" +new_without_default = "allow" + diff --git a/src/.DS_Store b/src/.DS_Store deleted file mode 100644 index b1cfadb..0000000 Binary files a/src/.DS_Store and /dev/null differ diff --git a/src/EK-MAC.itf b/src/EK-MAC.itf deleted file mode 100644 index 17b0b60..0000000 Binary files a/src/EK-MAC.itf and /dev/null differ diff --git a/src/app.rs b/src/app.rs deleted file mode 100644 index 611fc98..0000000 --- a/src/app.rs +++ /dev/null @@ -1,595 +0,0 @@ -use std::{ - collections::VecDeque, - fmt::Debug, - num::NonZero, - sync::{Arc, LazyLock, OnceLock}, - thread::JoinHandle, - time::Duration, -}; - -use smol::{channel::Sender, lock::Mutex}; -use torque_tracker_engine::{ - audio_processing::playback::PlaybackStatus, - manager::{AudioManager, OutputConfig, PlaybackSettings, ToWorkerMsg}, - project::song::{Song, SongOperation}, -}; -use triple_buffer::triple_buffer; -use winit::{ - application::ApplicationHandler, - event::{Modifiers, WindowEvent}, - event_loop::{ActiveEventLoop, ControlFlow, EventLoopProxy}, - keyboard::{Key, NamedKey}, - window::{Window, WindowAttributes}, -}; - -use cpal::{ - BufferSize, OutputStreamTimestamp, SupportedBufferSize, - traits::{DeviceTrait, HostTrait}, -}; - -use crate::{ - palettes::Palette, - ui::pages::{order_list::OrderListPageEvent, pattern::PatternPageEvent}, -}; - -use super::{ - draw_buffer::DrawBuffer, - render::RenderBackend, - ui::{ - dialog::{ - Dialog, DialogManager, DialogResponse, confirm::ConfirmDialog, page_menu::PageMenu, - }, - header::{Header, HeaderEvent}, - pages::{AllPages, PageEvent, PageResponse, PagesEnum}, - }, -}; - -pub static EXECUTOR: smol::Executor = smol::Executor::new(); -/// Song data -/// -/// Be careful about locking order with AUDIO_OUTPUT_COMMS to not deadlock -pub static SONG_MANAGER: LazyLock> = - LazyLock::new(|| Mutex::new(AudioManager::new(Song::default()))); -/// Sender for Song changes -pub static SONG_OP_SEND: OnceLock> = OnceLock::new(); - -/// shorter function name -pub fn send_song_op(op: SongOperation) { - SONG_OP_SEND.get().unwrap().send_blocking(op).unwrap(); -} - -pub enum GlobalEvent { - OpenDialog(Box Box + Send>), - Page(PageEvent), - Header(HeaderEvent), - /// also closes all dialogs - GoToPage(PagesEnum), - // Needed because only in the main app i know which pattern is selected, so i know what to play - Playback(PlaybackType), - CloseRequested, - CloseApp, - ConstRedraw, -} - -impl Clone for GlobalEvent { - fn clone(&self) -> Self { - // TODO: make this really clone, once the Dialogs are an enum instead of Box dyn - match self { - GlobalEvent::OpenDialog(_) => panic!("TODO: don't clone this"), - GlobalEvent::Page(page_event) => GlobalEvent::Page(page_event.clone()), - GlobalEvent::Header(header_event) => GlobalEvent::Header(header_event.clone()), - GlobalEvent::GoToPage(pages_enum) => GlobalEvent::GoToPage(*pages_enum), - GlobalEvent::CloseRequested => GlobalEvent::CloseRequested, - GlobalEvent::CloseApp => GlobalEvent::CloseApp, - GlobalEvent::ConstRedraw => GlobalEvent::ConstRedraw, - GlobalEvent::Playback(playback_type) => GlobalEvent::Playback(*playback_type), - } - } -} - -impl Debug for GlobalEvent { - fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { - let mut debug = f.debug_struct("GlobalEvent"); - match self { - GlobalEvent::OpenDialog(_) => debug.field("OpenDialog", &"closure"), - GlobalEvent::Page(page_event) => debug.field("Page", page_event), - GlobalEvent::Header(header_event) => debug.field("Header", header_event), - GlobalEvent::GoToPage(pages_enum) => debug.field("GoToPage", pages_enum), - GlobalEvent::CloseRequested => debug.field("CloseRequested", &""), - GlobalEvent::CloseApp => debug.field("CloseApp", &""), - GlobalEvent::ConstRedraw => debug.field("ConstRedraw", &""), - GlobalEvent::Playback(playback_type) => debug.field("Playback", &playback_type), - }; - debug.finish() - } -} - -#[derive(Clone, Copy, Debug)] -pub enum PlaybackType { - Stop, - Song, - Pattern, - FromOrder, - FromCursor, -} - -struct WorkerThreads { - handles: [JoinHandle<()>; 2], - close_msg: [Sender<()>; 2], -} - -impl WorkerThreads { - fn new() -> Self { - let (send1, recv1) = smol::channel::unbounded(); - let thread1 = std::thread::Builder::new() - .name("Background Worker 1".into()) - .spawn(Self::worker_task(recv1)) - .unwrap(); - let (send2, recv2) = smol::channel::unbounded(); - let thread2 = std::thread::Builder::new() - .name("Background Worker 2".into()) - .spawn(Self::worker_task(recv2)) - .unwrap(); - - Self { - handles: [thread1, thread2], - close_msg: [send1, send2], - } - } - - fn worker_task(recv: smol::channel::Receiver<()>) -> impl FnOnce() + Send + 'static { - move || { - smol::block_on(EXECUTOR.run(async { recv.recv().await.unwrap() })); - } - } - - /// prepares the closing of the threads by signalling them to stop - fn send_close(&mut self) { - _ = self.close_msg[0].send_blocking(()); - _ = self.close_msg[1].send_blocking(()); - } - - fn close_all(mut self) { - self.send_close(); - let [handle1, handle2] = self.handles; - handle1.join().unwrap(); - handle2.join().unwrap(); - } -} - -pub struct EventQueue<'a>(&'a mut VecDeque); - -impl EventQueue<'_> { - pub fn push(&mut self, event: GlobalEvent) { - self.0.push_back(event); - } -} - -pub struct App { - window_gpu: Option<(Arc, RenderBackend)>, - draw_buffer: DrawBuffer, - modifiers: Modifiers, - ui_pages: AllPages, - event_queue: VecDeque, - dialog_manager: DialogManager, - header: Header, - event_loop_proxy: EventLoopProxy, - worker_threads: Option, - // needed here because it isn't send. This Option should be synchronized with AUDIO_OUTPUT_COMMS - audio_stream: Option<( - cpal::Stream, - smol::Task<()>, - torque_tracker_engine::manager::StreamSend, - )>, -} - -impl ApplicationHandler for App { - fn new_events(&mut self, _: &ActiveEventLoop, start_cause: winit::event::StartCause) { - if start_cause == winit::event::StartCause::Init { - LazyLock::force(&SONG_MANAGER); - self.worker_threads = Some(WorkerThreads::new()); - let (send, recv) = smol::channel::unbounded(); - SONG_OP_SEND.get_or_init(|| send); - EXECUTOR - .spawn(async move { - while let Ok(op) = recv.recv().await { - let mut manager = SONG_MANAGER.lock().await; - // if there is no active channel the buffer isn't used, so it doesn't matter that it's wrong - let buffer_time = manager.last_buffer_time(); - // spin loop to lock the song - let mut song = loop { - if let Some(song) = manager.try_edit_song() { - break song; - } - // smol mutex lock is held across await point - smol::Timer::after(buffer_time).await; - }; - // apply the received op - song.apply_operation(op).unwrap(); - // try to get more ops. This avoids repeated locking of the song when a lot of operations are - // in queue - while let Ok(op) = recv.try_recv() { - song.apply_operation(op).unwrap(); - } - drop(song); - } - }) - .detach(); - // spawn a task to collect sample garbage every 10 seconds - EXECUTOR - .spawn(async { - loop { - let mut lock = SONG_MANAGER.lock().await; - lock.collect_garbage(); - drop(lock); - smol::Timer::after(Duration::from_secs(10)).await; - } - }) - .detach(); - self.start_audio_stream(); - } - } - - fn resumed(&mut self, event_loop: &winit::event_loop::ActiveEventLoop) { - self.build_window(event_loop); - } - - fn suspended(&mut self, _: &ActiveEventLoop) { - // my window and GPU state have been invalidated - self.window_gpu = None; - } - - fn window_event( - &mut self, - event_loop: &winit::event_loop::ActiveEventLoop, - _: winit::window::WindowId, - event: WindowEvent, - ) { - // destructure so i don't have to always type self. - let Self { - window_gpu, - draw_buffer, - modifiers, - ui_pages, - event_queue, - dialog_manager, - header, - event_loop_proxy: _, - worker_threads: _, - audio_stream: _, - } = self; - - // panic is fine because when i get a window_event a window exists - let (window, render_backend) = window_gpu.as_mut().unwrap(); - // don't want the window to be mut - let window = window.as_ref(); - // limit the pages and widgets to only push events and not read or pop - let event_queue = &mut EventQueue(event_queue); - - match event { - WindowEvent::CloseRequested => Self::close_requested(event_queue), - WindowEvent::Resized(physical_size) => { - render_backend.resize(physical_size); - window.request_redraw(); - } - WindowEvent::ScaleFactorChanged { - scale_factor: _, - inner_size_writer: _, - } => { - // window_state.resize(**new_inner_size); - // due to a version bump in winit i dont know anymore how to handle this event so i just ignore it for know and see if it makes problems in the future - // i have yet only received this event on linux wayland, not macos - println!("Window Scale Factor Changed"); - } - WindowEvent::RedrawRequested => { - // draw the new frame buffer - // TODO: split redraw header and redraw page. As soon as header gets a spectrometer this becomes important - header.draw(draw_buffer); - ui_pages.draw(draw_buffer); - dialog_manager.draw(draw_buffer); - // notify the windowing system that drawing is done and the new buffer is about to be pushed - window.pre_present_notify(); - // push the framebuffer into GPU/softbuffer and render it onto the screen - render_backend.render(&draw_buffer.framebuffer, event_loop); - } - WindowEvent::KeyboardInput { - device_id: _, - event, - is_synthetic, - } => { - if is_synthetic { - return; - } - - if event.state.is_pressed() { - if event.logical_key == Key::Named(NamedKey::F5) { - self.event_queue - .push_back(GlobalEvent::Playback(PlaybackType::Song)); - return; - } else if event.logical_key == Key::Named(NamedKey::F6) { - if modifiers.state().shift_key() { - self.event_queue - .push_back(GlobalEvent::Playback(PlaybackType::FromOrder)); - } else { - self.event_queue - .push_back(GlobalEvent::Playback(PlaybackType::Pattern)); - } - return; - } else if event.logical_key == Key::Named(NamedKey::F8) { - self.event_queue - .push_back(GlobalEvent::Playback(PlaybackType::Stop)); - return; - } - } - // key_event didn't start or stop the song, so process normally - if let Some(dialog) = dialog_manager.active_dialog_mut() { - match dialog.process_input(&event, modifiers, event_queue) { - DialogResponse::Close => { - dialog_manager.close_dialog(); - // if i close a pop_up i need to redraw the const part of the page as the pop-up overlapped it probably - ui_pages.request_draw_const(); - window.request_redraw(); - } - DialogResponse::RequestRedraw => window.request_redraw(), - DialogResponse::None => (), - } - } else { - if event.state.is_pressed() && event.logical_key == Key::Named(NamedKey::Escape) - { - event_queue.push(GlobalEvent::OpenDialog(Box::new(|| { - Box::new(PageMenu::main()) - }))); - } - - match ui_pages.process_key_event(&self.modifiers, &event, event_queue) { - PageResponse::RequestRedraw => window.request_redraw(), - PageResponse::None => (), - } - } - } - // not sure if i need it just to make sure i always have all current modifiers to be used with keyboard events - WindowEvent::ModifiersChanged(new_modifiers) => *modifiers = new_modifiers, - - _ => (), - } - - while let Some(event) = self.event_queue.pop_front() { - self.user_event(event_loop, event); - } - } - - /// i may need to add the ability for events to add events to the event queue, but that should be possible - fn user_event(&mut self, event_loop: &ActiveEventLoop, event: GlobalEvent) { - let event_queue = &mut EventQueue(&mut self.event_queue); - match event { - GlobalEvent::OpenDialog(dialog) => { - self.dialog_manager.open_dialog(dialog()); - _ = self.try_request_redraw(); - } - GlobalEvent::Page(c) => match self.ui_pages.process_page_event(c, event_queue) { - PageResponse::RequestRedraw => _ = self.try_request_redraw(), - PageResponse::None => (), - }, - GlobalEvent::Header(header_event) => { - self.header.process_event(header_event); - _ = self.try_request_redraw(); - } - GlobalEvent::GoToPage(pages_enum) => { - self.dialog_manager.close_all(); - self.ui_pages.switch_page(pages_enum); - _ = self.try_request_redraw(); - } - GlobalEvent::CloseApp => event_loop.exit(), - GlobalEvent::CloseRequested => Self::close_requested(event_queue), - GlobalEvent::ConstRedraw => { - self.ui_pages.request_draw_const(); - _ = self.try_request_redraw(); - } - GlobalEvent::Playback(playback_type) => { - let msg = match playback_type { - PlaybackType::Song => Some(ToWorkerMsg::Playback(PlaybackSettings::Order { - idx: 0, - should_loop: true, - })), - PlaybackType::Stop => Some(ToWorkerMsg::StopPlayback), - PlaybackType::Pattern => { - Some(ToWorkerMsg::Playback(self.header.play_current_pattern())) - } - PlaybackType::FromOrder => { - Some(ToWorkerMsg::Playback(self.header.play_current_order())) - } - PlaybackType::FromCursor => None, - }; - - if let Some(msg) = msg { - self.audio_stream - .as_mut() - .expect( - "audio stream should always be active, should still handle this error", - ) - .2 - .try_msg_worker(msg) - .expect("buffer full. either increase size or retry somehow") - } - } - } - } - - fn exiting(&mut self, _: &ActiveEventLoop) { - if let Some(workers) = self.worker_threads.take() { - // wait for all the threads to close - workers.close_all(); - } - if self.audio_stream.is_some() { - self.close_audio_stream(); - } - } -} - -impl App { - pub fn new(proxy: EventLoopProxy) -> Self { - Self { - window_gpu: None, - draw_buffer: DrawBuffer::new(), - modifiers: Modifiers::default(), - ui_pages: AllPages::new(proxy.clone()), - dialog_manager: DialogManager::new(), - header: Header::default(), - event_loop_proxy: proxy, - worker_threads: None, - audio_stream: None, - event_queue: VecDeque::with_capacity(3), - } - } - - // TODO: should this be its own function?? or is there something better - fn close_requested(events: &mut EventQueue<'_>) { - events.push(GlobalEvent::OpenDialog(Box::new(|| { - Box::new(ConfirmDialog::new( - "Close Torque Tracker?", - || Some(GlobalEvent::CloseApp), - || None, - )) - }))); - } - - /// tries to request a redraw. if there currently is no window this fails - fn try_request_redraw(&self) -> Result<(), ()> { - if let Some((window, _)) = &self.window_gpu { - window.request_redraw(); - Ok(()) - } else { - Err(()) - } - } - - fn build_window(&mut self, event_loop: &ActiveEventLoop) { - self.window_gpu.get_or_insert_with(|| { - let mut attributes = WindowAttributes::default(); - attributes.active = true; - attributes.resizable = true; - attributes.resize_increments = None; - attributes.title = String::from("Torque Tracker"); - - let window = Arc::new(event_loop.create_window(attributes).unwrap()); - let render_backend = RenderBackend::new(window.clone(), Palette::CAMOUFLAGE); - (window, render_backend) - }); - } - - // TODO: make this configurable - fn start_audio_stream(&mut self) { - assert!(self.audio_stream.is_none()); - let host = cpal::default_host(); - let device = host.default_output_device().unwrap(); - let default_config = device.default_output_config().unwrap(); - let (config, buffer_size) = { - let mut config = default_config.config(); - let buffer_size = { - let default = default_config.buffer_size(); - match default { - SupportedBufferSize::Unknown => 1024, - SupportedBufferSize::Range { min, max } => u32::min(u32::max(1024, *min), *max), - } - }; - config.buffer_size = BufferSize::Fixed(buffer_size); - (config, buffer_size) - }; - let mut guard = SONG_MANAGER.lock_blocking(); - let (mut worker, buffer_time, status, stream_send) = - guard.get_callback::(OutputConfig { - buffer_size, - channel_count: NonZero::new(config.channels).unwrap(), - sample_rate: NonZero::new(config.sample_rate.0).unwrap(), - }); - // keep the guard as short as possible to not block the async threads - drop(guard); - let (mut timestamp_send, recv) = triple_buffer(&None); - let stream = device - .build_output_stream( - &config, - move |data, info| { - worker(data); - timestamp_send.write(Some(info.timestamp())); - }, - |err| eprintln!("audio stream err: {err:?}"), - None, - ) - .unwrap(); - // spawn a task to process the audio playback status updates - let proxy = self.event_loop_proxy.clone(); - let task = EXECUTOR.spawn(async move { - let buffer_time = buffer_time; - let mut status_recv = status; - // maybe also send the timestamp every second or so - let mut timestamp_recv = recv; - let mut old_status: Option = None; - let mut old_timestamp: Option = None; - loop { - let status = *status_recv.get(); - // only react on status changes. could at some point be made more granular - if status != old_status { - old_status = status; - // println!("playback status: {status:?}"); - let pos = status.map(|s| s.position); - proxy - .send_event(GlobalEvent::Header(HeaderEvent::SetPlayback(pos))) - .unwrap(); - let pos = status.map(|s| (s.position.pattern, s.position.row)); - proxy - .send_event(GlobalEvent::Page(PageEvent::Pattern( - PatternPageEvent::PlaybackPosition(pos), - ))) - .unwrap(); - // does a map flatten. idk why it's called and_then - let pos = status.and_then(|s| s.position.order); - proxy - .send_event(GlobalEvent::Page(PageEvent::OrderList( - OrderListPageEvent::SetPlayback(pos), - ))) - .unwrap(); - } - let timestamp = *timestamp_recv.read(); - if timestamp != old_timestamp { - // TODO: maybe send it somewhere - old_timestamp = timestamp; - } - smol::Timer::after(buffer_time).await; - } - }); - self.audio_stream = Some((stream, task, stream_send)); - } - - fn close_audio_stream(&mut self) { - let (stream, task, mut stream_send) = self.audio_stream.take().unwrap(); - // stop playback - _ = stream_send.try_msg_worker(ToWorkerMsg::StopPlayback); - _ = stream_send.try_msg_worker(ToWorkerMsg::StopLiveNote); - // kill the task. using `cancel` doesn't make sense because it doesn't finishe anyways - drop(task); - // lastly kill the audio stream - drop(stream); - } -} - -impl Drop for App { - fn drop(&mut self) { - if self.audio_stream.is_some() { - self.close_audio_stream(); - } - } -} - -pub fn run() { - let event_loop = winit::event_loop::EventLoop::::with_user_event() - .build() - .unwrap(); - event_loop.set_control_flow(ControlFlow::Wait); - // i don't need any raw device events. Keyboard and Mouse coming as window events are enough - event_loop.listen_device_events(winit::event_loop::DeviceEvents::Never); - let event_loop_proxy = event_loop.create_proxy(); - let mut app = App::new(event_loop_proxy); - app.header.draw_constant(&mut app.draw_buffer); - - event_loop.run_app(&mut app).unwrap(); -} diff --git a/src/coordinates.rs b/src/coordinates.rs index 6468eef..c7b914e 100644 --- a/src/coordinates.rs +++ b/src/coordinates.rs @@ -1,22 +1,24 @@ use std::ops::{Add, RangeInclusive}; /// font size in pixel. font is a square -pub const FONT_SIZE: usize = 8; +pub const FONT_SIZE: u8 = 8; +pub const FONT_SIZE16: u16 = FONT_SIZE as u16; /// window size in characters -pub const WINDOW_SIZE_CHARS: (usize, usize) = (80, 50); +pub const WINDOW_SIZE_CHARS: (u8, u8) = (80, 50); /// window size in pixel -pub const WINDOW_SIZE: (usize, usize) = ( - FONT_SIZE * WINDOW_SIZE_CHARS.0, - FONT_SIZE * WINDOW_SIZE_CHARS.1, +pub const WINDOW_SIZE: (u16, u16) = ( + // TODO: use from once const traits are available + FONT_SIZE16 * WINDOW_SIZE_CHARS.0 as u16, + FONT_SIZE16 * WINDOW_SIZE_CHARS.1 as u16, ); /// CharRect as well as PixelRect uses all values inclusive, meaning the borders are included #[derive(Debug, Clone, Copy)] pub struct CharRect { - top: usize, - bot: usize, - left: usize, - right: usize, + top: u8, + bot: u8, + left: u8, + right: u8, } impl CharRect { @@ -24,7 +26,7 @@ impl CharRect { pub const PAGE_AREA: Self = Self::new(12, WINDOW_SIZE_CHARS.1 - 1, 0, WINDOW_SIZE_CHARS.0 - 1); pub const HEADER_AREA: Self = Self::new(0, 10, 0, WINDOW_SIZE_CHARS.0 - 1); - pub const fn new(top: usize, bot: usize, left: usize, right: usize) -> Self { + pub const fn new(top: u8, bot: u8, left: u8, right: u8) -> Self { assert!(top <= bot, "top needs to be smaller than bot"); assert!(left <= right, "left needs to be smaller than right"); assert!(bot < WINDOW_SIZE_CHARS.1, "lower than window bounds"); @@ -38,16 +40,16 @@ impl CharRect { } } - pub const fn top(self) -> usize { + pub const fn top(self) -> u8 { self.top } - pub const fn bot(self) -> usize { + pub const fn bot(self) -> u8 { self.bot } - pub const fn right(self) -> usize { + pub const fn right(self) -> u8 { self.right } - pub const fn left(self) -> usize { + pub const fn left(self) -> u8 { self.left } @@ -58,39 +60,44 @@ impl CharRect { } } - pub const fn width(self) -> usize { + pub const fn width(self) -> u8 { self.right - self.left } - pub const fn height(self) -> usize { + pub const fn height(self) -> u8 { self.bot - self.top } } -/// uncheck conversion, because CharPosition is a safe type impl From for CharRect { fn from(value: CharPosition) -> Self { Self::new(value.y, value.y, value.x, value.x) - // Self { - // top: value.y, - // bot: value.y, - // left: value.x, - // right: value.x, - // } + } +} + +#[cfg(feature = "accesskit")] +impl From for accesskit::Rect { + fn from(value: CharRect) -> Self { + accesskit::Rect { + x0: value.left as f64, + y0: value.top as f64, + x1: value.right as f64, + y1: value.bot as f64, + } } } /// PixelRect as well as CharRect uses all values inclusive, meaning the borders are included #[derive(Debug, Clone, Copy)] pub struct PixelRect { - top: usize, - bot: usize, - right: usize, - left: usize, + top: u16, + bot: u16, + right: u16, + left: u16, } impl PixelRect { - pub const fn new(top: usize, bot: usize, right: usize, left: usize) -> Self { + pub const fn new(top: u16, bot: u16, right: u16, left: u16) -> Self { assert!(top <= bot, "top needs to be smaller than bot"); assert!(left <= right, "left needs to be smaller than right"); assert!(bot < WINDOW_SIZE.1, "lower than window bounds"); @@ -104,50 +111,42 @@ impl PixelRect { } } - pub const fn vertical_range(&self) -> RangeInclusive { + pub const fn vertical_range(&self) -> RangeInclusive { RangeInclusive::new(self.top, self.bot) } - pub const fn horizontal_range(&self) -> RangeInclusive { + pub const fn horizontal_range(&self) -> RangeInclusive { RangeInclusive::new(self.left, self.right) } - pub const fn top(&self) -> usize { + pub const fn top(&self) -> u16 { self.top } - pub const fn bot(&self) -> usize { + pub const fn bot(&self) -> u16 { self.bot } - pub const fn right(&self) -> usize { + pub const fn right(&self) -> u16 { self.right } - pub const fn left(&self) -> usize { + pub const fn left(&self) -> u16 { self.left } } -/// unchecked conversion because CharRect is a safe type impl From for PixelRect { fn from(value: CharRect) -> Self { Self::new( - value.top * FONT_SIZE, - (value.bot * FONT_SIZE) + FONT_SIZE - 1, - (value.right * FONT_SIZE) + FONT_SIZE - 1, - value.left * FONT_SIZE, + u16::from(value.top) * FONT_SIZE16, + u16::from(value.bot) * FONT_SIZE16 + FONT_SIZE16 - 1, + u16::from(value.right) * FONT_SIZE16 + FONT_SIZE16 - 1, + u16::from(value.left) * FONT_SIZE16, ) - // Self { - // top: value.top * FONT_SIZE, - // bot: (value.bot * FONT_SIZE) + FONT_SIZE - 1, - // right: (value.right * FONT_SIZE) + FONT_SIZE - 1, - // left: value.left * FONT_SIZE, - // } } } -/// unchecked conversion, because CharPosition is a safe type impl From for PixelRect { fn from(value: CharPosition) -> Self { Self::from(CharRect::from(value)) @@ -156,24 +155,24 @@ impl From for PixelRect { #[derive(Debug, Clone, Copy)] pub struct CharPosition { - x: usize, - y: usize, + x: u8, + y: u8, } impl CharPosition { #[track_caller] - pub const fn new(x: usize, y: usize) -> Self { + pub const fn new(x: u8, y: u8) -> Self { assert!(y < WINDOW_SIZE_CHARS.1); assert!(x < WINDOW_SIZE_CHARS.0); Self { x, y } } - pub const fn x(&self) -> usize { + pub const fn x(&self) -> u8 { self.x } - pub const fn y(&self) -> usize { + pub const fn y(&self) -> u8 { self.y } } @@ -187,11 +186,11 @@ impl Add for CharPosition { } } -impl Add<(usize, usize)> for CharPosition { +impl Add<(u8, u8)> for CharPosition { type Output = Self; #[track_caller] - fn add(self, rhs: (usize, usize)) -> Self::Output { + fn add(self, rhs: (u8, u8)) -> Self::Output { Self::new(self.x + rhs.0, self.y + rhs.1) } } diff --git a/src/dialog/confirm.rs b/src/dialog/confirm.rs new file mode 100644 index 0000000..2adfc39 --- /dev/null +++ b/src/dialog/confirm.rs @@ -0,0 +1,149 @@ +use winit::keyboard::{Key, NamedKey}; + +use crate::{ + GlobalEvent, + coordinates::{CharPosition, CharRect}, + draw_buffer::DrawBuffer, + widgets::{NextWidget, Widget, WidgetResponse, button::Button}, +}; + +use super::{Dialog, DialogResponse}; + +pub struct ConfirmDialog { + text: &'static str, + text_pos: CharPosition, + // computed from the string length + rect: CharRect, + ok: (Button, Option GlobalEvent>), + cancel: (Button, Option GlobalEvent>), + selected: u8, +} + +impl ConfirmDialog { + const DIALOG_ID: u64 = 600_000_000; + + const OK_RECT: CharRect = CharRect::new(29, 31, 41, 50); + const CANCEL_RECT: CharRect = CharRect::new(29, 31, 30, 39); + const OK: u8 = 1; + const CANCEL: u8 = 2; + pub fn new( + text: &'static str, + ok_event: Option GlobalEvent>, + cancel_event: Option GlobalEvent>, + ) -> Self { + let width = (u8::try_from(text.len()).unwrap() + 10).max(22); + let per_side = width / 2; + Self { + text, + text_pos: CharPosition::new(40 - per_side + 5, 27), + selected: Self::OK, + ok: ( + Button::new( + " Ok", + Self::OK_RECT, + NextWidget { + left: Some(Self::CANCEL), + right: Some(Self::CANCEL), + tab: Some(Self::CANCEL), + shift_tab: Some(Self::CANCEL), + ..Default::default() + }, + #[cfg(feature = "accesskit")] + accesskit::NodeId(Self::DIALOG_ID + u64::from(Self::OK) * 20), + ), + ok_event, + ), + cancel: ( + Button::new( + "Cancel", + Self::CANCEL_RECT, + NextWidget { + left: Some(Self::OK), + right: Some(Self::OK), + tab: Some(Self::OK), + shift_tab: Some(Self::OK), + ..Default::default() + }, + #[cfg(feature = "accesskit")] + accesskit::NodeId(Self::DIALOG_ID + u64::from(Self::CANCEL) * 20), + ), + cancel_event, + ), + rect: CharRect::new(25, 32, 40 - per_side, 40 + per_side), + } + } +} + +impl Dialog for ConfirmDialog { + fn draw(&self, draw_buffer: &mut DrawBuffer) { + draw_buffer.draw_rect(2, self.rect); + draw_buffer.draw_out_border(self.rect, 3, 3, 2); + draw_buffer.draw_string(self.text, self.text_pos, 0, 2); + self.ok.0.draw(draw_buffer, self.selected == Self::OK); + self.cancel + .0 + .draw(draw_buffer, self.selected == Self::CANCEL); + } + + fn process_input( + &mut self, + key_event: &winit::event::KeyEvent, + modifiers: &winit::event::Modifiers, + events: &mut crate::EventQueue<'_>, + ) -> DialogResponse { + if key_event.logical_key == Key::Named(NamedKey::Escape) && modifiers.state().is_empty() { + return DialogResponse::Close; + } + + let response = match self.selected { + Self::OK => { + let resp = self.ok.0.process_input(modifiers, key_event, events); + if resp == WidgetResponse::RequestRedraw(true) { + if let Some(event) = self.ok.1.take() { + events.push(event()); + } + return DialogResponse::Close; + } + resp + } + Self::CANCEL => { + let resp = self.cancel.0.process_input(modifiers, key_event, events); + if resp == WidgetResponse::RequestRedraw(true) { + if let Some(event) = self.cancel.1.take() { + events.push(event()); + } + return DialogResponse::Close; + } + resp + } + _ => unreachable!(), + }; + + match response { + WidgetResponse::SwitchFocus(i) => { + self.selected = i; + DialogResponse::RequestRedraw + } + // always false here, because otherwise we return earlier, but it doesn't matter + WidgetResponse::RequestRedraw(_) => DialogResponse::RequestRedraw, + WidgetResponse::None => DialogResponse::None, + } + } + + #[cfg(feature = "accesskit")] + fn build_tree( + &self, + tree: &mut Vec<(accesskit::NodeId, accesskit::Node)>, + ) -> crate::AccessResponse { + use accesskit::{Node, Role}; + + use crate::AccessResponse; + + let mut root_node = Node::new(Role::Dialog); + + AccessResponse { + root: todo!(), + selected: todo!(), + } + } +} diff --git a/src/dialog/mod.rs b/src/dialog/mod.rs new file mode 100644 index 0000000..ccccebf --- /dev/null +++ b/src/dialog/mod.rs @@ -0,0 +1,140 @@ +pub mod confirm; +pub mod page_menu; +pub mod slider_dialog; + +use winit::event::{KeyEvent, Modifiers}; + +use crate::{ + EventQueue, + dialog::{confirm::ConfirmDialog, page_menu::MainMenu, slider_dialog::SliderDialog}, + draw_buffer::DrawBuffer, +}; + +#[derive(PartialEq, Eq)] +pub enum DialogResponse { + RequestRedraw, + // should also close all Dialogs + // SwitchToPage(PagesEnum), + Close, + /// (global_event to be sent, should close the current dialog) + // GlobalEvent(GlobalEvent, bool), + None, +} + +pub trait Dialog { + fn draw(&self, draw_buffer: &mut DrawBuffer); + fn process_input( + &mut self, + key_event: &KeyEvent, + modifiers: &Modifiers, + events: &mut EventQueue<'_>, + ) -> DialogResponse; + #[cfg(feature = "accesskit")] + fn build_tree( + &self, + tree: &mut Vec<(accesskit::NodeId, accesskit::Node)>, + ) -> crate::AccessResponse; +} + +pub struct DialogManager { + stack: Vec, +} + +pub enum DialogEnum { + Main(MainMenu), + Slider(SliderDialog), + Confirm(ConfirmDialog), +} + +impl DialogEnum { + fn get_mut(&mut self) -> &mut dyn Dialog { + match self { + DialogEnum::Main(d) => d, + DialogEnum::Slider(d) => d, + DialogEnum::Confirm(d) => d, + } + } + + fn get(&self) -> &dyn Dialog { + match self { + DialogEnum::Main(d) => d, + DialogEnum::Slider(d) => d, + DialogEnum::Confirm(d) => d, + } + } +} + +impl Dialog for DialogEnum { + fn draw(&self, draw_buffer: &mut DrawBuffer) { + match self { + DialogEnum::Main(d) => d.draw(draw_buffer), + DialogEnum::Slider(d) => d.draw(draw_buffer), + DialogEnum::Confirm(d) => d.draw(draw_buffer), + } + } + + fn process_input( + &mut self, + key_event: &KeyEvent, + modifiers: &Modifiers, + events: &mut EventQueue<'_>, + ) -> DialogResponse { + match self { + DialogEnum::Main(d) => d.process_input(key_event, modifiers, events), + DialogEnum::Slider(d) => d.process_input(key_event, modifiers, events), + DialogEnum::Confirm(d) => d.process_input(key_event, modifiers, events), + } + } + + #[cfg(feature = "accesskit")] + fn build_tree( + &self, + tree: &mut Vec<(accesskit::NodeId, accesskit::Node)>, + ) -> crate::AccessResponse { + match self { + DialogEnum::Main(d) => d.build_tree(tree), + DialogEnum::Slider(d) => d.build_tree(tree), + DialogEnum::Confirm(d) => d.build_tree(tree), + } + } +} + +impl DialogManager { + pub fn new() -> Self { + // try to match the capacity to the actually used maximum depth + Self { + stack: Vec::with_capacity(3), + } + } + + pub fn active_dialog_mut(&mut self) -> Option<&mut dyn Dialog> { + self.stack.last_mut().map(|d| d.get_mut()) + } + + pub fn active_dialog(&self) -> Option<&dyn Dialog> { + self.stack.last().map(|d| d.get()) + } + + pub fn is_active(&self) -> bool { + !self.stack.is_empty() + } + + pub fn open_dialog(&mut self, dialog: DialogEnum) { + self.stack.push(dialog); + } + + pub fn close_dialog(&mut self) { + self.stack.pop(); + } + + pub fn close_all(&mut self) { + self.stack.clear(); + } + + /// draws all currently open dialogs + pub fn draw(&self, draw_buffer: &mut DrawBuffer) { + self.stack + .iter() + .for_each(|dialog| dialog.draw(draw_buffer)); + } +} diff --git a/src/ui/dialog/page_menu.rs b/src/dialog/page_menu.rs similarity index 55% rename from src/ui/dialog/page_menu.rs rename to src/dialog/page_menu.rs index 225eb11..befc778 100644 --- a/src/ui/dialog/page_menu.rs +++ b/src/dialog/page_menu.rs @@ -1,24 +1,26 @@ use winit::keyboard::{Key, NamedKey}; use crate::{ - app::{EventQueue, GlobalEvent, PlaybackType}, - coordinates::{CharPosition, CharRect, FONT_SIZE, PixelRect}, + EventQueue, GlobalEvent, PlaybackType, + coordinates::{CharPosition, CharRect, FONT_SIZE16, PixelRect}, draw_buffer::DrawBuffer, - ui::pages::PagesEnum, + pages::PagesEnum, }; use super::{Dialog, DialogResponse}; enum Action { Menu(Menu), - // TODO: maybe fold this into the more general event variant Page(PagesEnum), - Event(GlobalEvent), + Playback(PlaybackType), + // const data. Allows Recursing this inside the GlobalEvent + Event(fn() -> GlobalEvent), // TODO: should be removed when it's all implemented NotYetImplemented, } // Main missing, because it cant be opened from a menu +#[derive(Clone, Copy)] enum Menu { File, Playback, @@ -30,14 +32,68 @@ enum Menu { pub struct PageMenu { name: &'static str, rect: CharRect, - selected: usize, + selected: u8, pressed: bool, buttons: &'static [(&'static str, Action)], - sub_menu: Option>, + #[cfg(feature = "accesskit")] + node_id: accesskit::NodeId, } -impl Dialog for PageMenu { - fn draw(&self, draw_buffer: &mut DrawBuffer) { +impl PageMenu { + const BACKGROUND_COLOR: u8 = 2; + const TOPLEFT_COLOR: u8 = 3; + const BOTRIGHT_COLOR: u8 = 1; + const fn new( + name: &'static str, + pos: CharPosition, + width: u8, + buttons: &'static [(&'static str, Action)], + #[cfg(feature = "accesskit")] node_id: accesskit::NodeId, + ) -> Self { + let rect = CharRect::new( + pos.y(), + // TODO: const trait + pos.y() + 5 + (3 * buttons.len() as u8), + pos.x(), + pos.x() + width + 3, + ); + + Self { + name, + rect, + selected: 0, + pressed: false, + buttons, + #[cfg(feature = "accesskit")] + node_id, + } + } + fn draw_button_corners(rect: CharRect, draw_buffer: &mut DrawBuffer) { + // let framebuffer = &mut draw_buffer.framebuffer; + let pixel_rect = PixelRect::from(rect); + // draw top right corner + for y in 0..FONT_SIZE16 { + for x in y..FONT_SIZE16 { + draw_buffer.set( + pixel_rect.top() + y, + pixel_rect.right() - FONT_SIZE16 + x + 1, + Self::BOTRIGHT_COLOR, + ); + } + } + // draw botleft corner + for y in 0..FONT_SIZE16 { + for x in 0..(FONT_SIZE16 - y) { + draw_buffer.set( + pixel_rect.bot() - y, + pixel_rect.left() + x, + Self::BOTRIGHT_COLOR, + ); + } + } + } + + fn draw(&self, has_child: bool, draw_buffer: &mut DrawBuffer) { let top_left = self.rect.top_left(); let top = top_left.y(); let left = top_left.x(); @@ -58,6 +114,7 @@ impl Dialog for PageMenu { 1, ); for (num, (name, _)) in self.buttons.iter().enumerate() { + let num = u8::try_from(num).unwrap(); let text_color = match self.selected == num { true => 11, false => 0, @@ -70,69 +127,46 @@ impl Dialog for PageMenu { Self::BACKGROUND_COLOR, ); let top = top + (3 * num) + 4; - let (top_left, bot_right) = - match (self.pressed || self.sub_menu.is_some()) && self.selected == num { - true => (Self::BOTRIGHT_COLOR, Self::TOPLEFT_COLOR), - false => (Self::TOPLEFT_COLOR, Self::BOTRIGHT_COLOR), - }; + let (top_left, bot_right) = match (self.pressed || has_child) && self.selected == num { + true => (Self::BOTRIGHT_COLOR, Self::TOPLEFT_COLOR), + false => (Self::TOPLEFT_COLOR, Self::BOTRIGHT_COLOR), + }; let rect = CharRect::new(top, top + 2, left + 2, left + self.rect.width() - 2); draw_buffer.draw_out_border(rect, top_left, bot_right, 1); Self::draw_button_corners(rect, draw_buffer); } - if let Some(sub) = self.sub_menu.as_ref() { - sub.draw(draw_buffer); - } } fn process_input( &mut self, key_event: &winit::event::KeyEvent, - modifiers: &winit::event::Modifiers, - event: &mut EventQueue<'_>, - ) -> DialogResponse { - if key_event.state.is_pressed() && key_event.logical_key == Key::Named(NamedKey::Escape) { - if self.sub_menu.is_some() { - self.sub_menu = None; - event.push(GlobalEvent::ConstRedraw); - return DialogResponse::RequestRedraw; - } else { - return DialogResponse::Close; - } - } - - if let Some(sub) = self.sub_menu.as_mut() { - return sub.process_input(key_event, modifiers, event); - } - + events: &mut EventQueue<'_>, + ) -> (DialogResponse, Option) { if key_event.logical_key == Key::Named(NamedKey::Enter) { if key_event.state.is_pressed() { self.pressed = true; - return DialogResponse::RequestRedraw; + return (DialogResponse::RequestRedraw, None); } else if self.pressed { self.pressed = false; - match &self.buttons[self.selected].1 { + match &self.buttons[usize::from(self.selected)].1 { Action::Menu(menu) => { - let menu = match menu { - Menu::File => Self::file(), - Menu::Playback => Self::playback(), - Menu::Sample => Self::sample(), - Menu::Instrument => Self::instrument(), - Menu::Settings => Self::settings(), - }; - self.sub_menu = Some(Box::new(menu)); - return DialogResponse::RequestRedraw; + return (DialogResponse::RequestRedraw, Some(*menu)); } Action::Page(page) => { - event.push(GlobalEvent::GoToPage(*page)); - return DialogResponse::Close; + events.push(GlobalEvent::GoToPage(*page)); + return (DialogResponse::Close, None); } Action::NotYetImplemented => { - println!("Not yet implementes"); - return DialogResponse::RequestRedraw; + eprintln!("Not yet implementes"); + return (DialogResponse::RequestRedraw, None); } Action::Event(global_event) => { - event.push(global_event.clone()); - return DialogResponse::Close; + events.push(global_event()); + return (DialogResponse::Close, None); + } + Action::Playback(p) => { + events.push(GlobalEvent::Playback(*p)); + return (DialogResponse::Close, None); } } } @@ -142,65 +176,139 @@ impl Dialog for PageMenu { if key_event.logical_key == Key::Named(NamedKey::ArrowUp) && self.selected > 0 { self.selected -= 1; self.pressed = false; - return DialogResponse::RequestRedraw; + return (DialogResponse::RequestRedraw, None); } else if key_event.logical_key == Key::Named(NamedKey::ArrowDown) - && self.selected < self.buttons.len() - 1 + && usize::from(self.selected) < self.buttons.len() - 1 { self.selected += 1; self.pressed = false; - return DialogResponse::RequestRedraw; + return (DialogResponse::RequestRedraw, None); + } + } + + (DialogResponse::None, None) + } + + #[cfg(feature = "accesskit")] + fn build_tree( + &self, + tree: &mut Vec<(accesskit::NodeId, accesskit::Node)>, + ) -> crate::AccessResponse { + use accesskit::{Action, Node, NodeId, Rect, Role}; + + use crate::AccessResponse; + let mut root = Node::new(Role::Dialog); + root.set_label(self.name); + root.set_bounds(dbg!(Rect::from(self.rect))); + let selected = NodeId(u64::from(self.selected) + self.node_id.0 + 1); + + // root of the sub_menu. Will be set as the child of the selected button + // let sub_menu = self.sub_menu.as_ref().map(|m| { + // let resp = m.build_tree(tree); + + // selected = resp.selected; + + // resp.root + // }); + + for (num, (name, _)) in self.buttons.iter().enumerate() { + let mut node = Node::new(Role::Button); + node.add_action(Action::Click); + node.set_keyboard_shortcut("Enter"); + node.set_label(*name); + if usize::from(self.selected) == num { + node.set_selected(true); + // if let Some(id) = sub_menu { + // node.push_child(id); + // } } + let id = NodeId(u64::try_from(num).unwrap() + self.node_id.0 + 1); + tree.push((id, node)); + root.push_child(id); } - DialogResponse::None + tree.push((self.node_id, root)); + AccessResponse { + root: self.node_id, + selected, + } } } -impl PageMenu { - const BACKGROUND_COLOR: u8 = 2; - const TOPLEFT_COLOR: u8 = 3; - const BOTRIGHT_COLOR: u8 = 1; - const fn new( - name: &'static str, - pos: CharPosition, - width: usize, - buttons: &'static [(&'static str, Action)], - ) -> Self { - let rect = CharRect::new( - pos.y(), - pos.y() + 5 + (3 * buttons.len()), - pos.x(), - pos.x() + width + 3, - ); +pub struct MainMenu { + main: PageMenu, + child: Option, +} - Self { - name, - rect, - selected: 0, - pressed: false, - buttons, - sub_menu: None, +impl Dialog for MainMenu { + fn draw(&self, draw_buffer: &mut DrawBuffer) { + self.main.draw(self.child.is_some(), draw_buffer); + if let Some(child) = &self.child { + // the child never has a child + child.draw(false, draw_buffer); } } - fn draw_button_corners(rect: CharRect, draw_buffer: &mut DrawBuffer) { - let framebuffer = &mut draw_buffer.framebuffer; - let pixel_rect = PixelRect::from(rect); - // draw top right corner - for y in 0..FONT_SIZE { - for x in y..FONT_SIZE { - framebuffer[pixel_rect.top() + y][pixel_rect.right() - FONT_SIZE + x + 1] = - Self::BOTRIGHT_COLOR; + fn process_input( + &mut self, + key_event: &winit::event::KeyEvent, + _modifiers: &winit::event::Modifiers, + events: &mut EventQueue<'_>, + ) -> DialogResponse { + // check for close dialog + if key_event.state.is_pressed() && key_event.logical_key == Key::Named(NamedKey::Escape) { + if self.child.is_some() { + self.child = None; + events.push(GlobalEvent::ConstRedraw); + return DialogResponse::RequestRedraw; + } else { + return DialogResponse::Close; } } - // draw botleft corner - for y in 0..FONT_SIZE { - for x in 0..(FONT_SIZE - y) { - framebuffer[pixel_rect.bot() - y][pixel_rect.left() + x] = Self::BOTRIGHT_COLOR; + + if let Some(child) = &mut self.child { + let (resp, open_child) = child.process_input(key_event, events); + assert!(open_child.is_none(), "the child can't open a child"); + resp + } else { + let (resp, open_child) = self.main.process_input(key_event, events); + if let Some(open_child) = open_child { + assert!( + resp == DialogResponse::RequestRedraw, + "child open is always in combination with redraw" + ); + let child = match open_child { + Menu::File => PageMenu::file(), + Menu::Playback => PageMenu::playback(), + Menu::Sample => PageMenu::sample(), + Menu::Instrument => PageMenu::instrument(), + Menu::Settings => PageMenu::settings(), + }; + self.child = Some(child); } + resp } } + #[cfg(feature = "accesskit")] + fn build_tree( + &self, + _tree: &mut Vec<(accesskit::NodeId, accesskit::Node)>, + ) -> crate::AccessResponse { + todo!("look at PageMenu commented out stuff"); + } +} + +impl MainMenu { + pub fn new() -> Self { + Self { + main: PageMenu::main(), + child: None, + } + } +} + +impl PageMenu { pub const fn main() -> Self { Self::new( "Main Menu", @@ -227,6 +335,8 @@ impl PageMenu { ("Settings Menu...", Action::Menu(Menu::Settings)), ("Help! (F1)", Action::Page(PagesEnum::Help)), ], + #[cfg(feature = "accesskit")] + accesskit::NodeId(1_000_000_000), ) } @@ -244,9 +354,11 @@ impl PageMenu { ("Message Log (Ctrl-F11)", Action::NotYetImplemented), ( "Quit (Ctrl-Q)", - Action::Event(GlobalEvent::CloseRequested), + Action::Event(|| GlobalEvent::CloseRequested), ), ], + #[cfg(feature = "accesskit")] + accesskit::NodeId(1_000_000_100), ) } @@ -259,25 +371,27 @@ impl PageMenu { ("Show Infopage (F5)", Action::NotYetImplemented), ( "Play Song (Ctrl-F5)", - Action::Event(GlobalEvent::Playback(PlaybackType::Song)), + Action::Playback(PlaybackType::Song), ), ( "Play Pattern (F6)", - Action::Event(GlobalEvent::Playback(PlaybackType::Pattern)), + Action::Playback(PlaybackType::Pattern), ), ( "Play from Order (Shift-F6)", - Action::Event(GlobalEvent::Playback(PlaybackType::FromOrder)), + Action::Playback(PlaybackType::FromOrder), ), ("Play from Mark/Cursor (F7)", Action::NotYetImplemented), ( "Stop (F8)", - Action::Event(GlobalEvent::Playback(PlaybackType::Stop)), + Action::Playback(PlaybackType::Stop), ), ("Reinit Soundcard (Ctrl-I)", Action::NotYetImplemented), ("Driver Screen (Shift-F5)", Action::NotYetImplemented), ("Calculate Length (Ctrl-P)", Action::NotYetImplemented), ], + #[cfg(feature = "accesskit")] + accesskit::NodeId(1_000_000_200), ) } @@ -293,6 +407,8 @@ impl PageMenu { ), ("Sample Library (Ctrl-F3)", Action::NotYetImplemented), ], + #[cfg(feature = "accesskit")] + accesskit::NodeId(1_000_000_300), ) } @@ -305,6 +421,8 @@ impl PageMenu { ("Instrument List (F4)", Action::NotYetImplemented), ("Instrument Library (Ctrl-F4)", Action::NotYetImplemented), ], + #[cfg(feature = "accesskit")] + accesskit::NodeId(1_000_000_400), ) } @@ -339,6 +457,8 @@ impl PageMenu { Action::NotYetImplemented, ), ], + #[cfg(feature = "accesskit")] + accesskit::NodeId(1_000_000_500), ) } } diff --git a/src/ui/dialog/slider_dialog.rs b/src/dialog/slider_dialog.rs similarity index 53% rename from src/ui/dialog/slider_dialog.rs rename to src/dialog/slider_dialog.rs index e975b91..90be49b 100644 --- a/src/ui/dialog/slider_dialog.rs +++ b/src/dialog/slider_dialog.rs @@ -6,16 +6,16 @@ use winit::{ }; use crate::{ - app::{EventQueue, GlobalEvent}, + EventQueue, GlobalEvent, coordinates::{CharPosition, CharRect}, draw_buffer::DrawBuffer, - ui::widgets::{NextWidget, StandardResponse, Widget, text_in::TextIn}, + widgets::{NextWidget, Widget, WidgetResponse, text_in::TextIn}, }; use super::{Dialog, DialogResponse}; pub struct SliderDialog { - text: TextIn<()>, + text: TextIn, range: RangeInclusive, return_event: fn(i16) -> GlobalEvent, } @@ -48,26 +48,59 @@ impl Dialog for SliderDialog { } } - match self - .text - .process_input(modifiers, key_event, events) - .standard - { + match self.text.process_input(modifiers, key_event, events) { // cant switch focus as this is the only widget - StandardResponse::SwitchFocus(_) => DialogResponse::None, - StandardResponse::RequestRedraw => DialogResponse::RequestRedraw, - StandardResponse::None => DialogResponse::None, + WidgetResponse::SwitchFocus(_) => DialogResponse::None, + WidgetResponse::RequestRedraw(_) => DialogResponse::RequestRedraw, + WidgetResponse::None => DialogResponse::None, + } + } + + #[cfg(feature = "accesskit")] + fn build_tree( + &self, + tree: &mut Vec<(accesskit::NodeId, accesskit::Node)>, + ) -> crate::AccessResponse { + use accesskit::{Node, NodeId, Role}; + + use crate::AccessResponse; + + const ROOT_ID: NodeId = NodeId(400_000_000); + const TEXT_ID: NodeId = NodeId(400_000_001); + + let mut root_node = Node::new(Role::Dialog); + root_node.set_label("Slider Dialog"); + root_node.push_child(TEXT_ID); + + let mut text_node = Node::new(Role::NumberInput); + text_node.set_min_numeric_value(*self.range.start() as f64); + text_node.set_max_numeric_value(*self.range.end() as f64); + text_node.set_value(self.text.get_str()); + text_node.set_label("Set Value"); + + tree.push((ROOT_ID, root_node)); + tree.push((TEXT_ID, text_node)); + AccessResponse { + root: ROOT_ID, + selected: TEXT_ID, } } } impl SliderDialog { + const NODE_ID: u64 = 400_000_000; pub fn new( inital_char: char, range: RangeInclusive, return_event: fn(i16) -> GlobalEvent, ) -> Self { - let mut text_in = TextIn::new(CharPosition::new(45, 26), 3, NextWidget::default(), |_| {}); + let mut text_in = TextIn::new( + CharPosition::new(45, 26), + 3, + NextWidget::default(), + #[cfg(feature = "accesskit")] + (accesskit::NodeId(Self::NODE_ID + 20), "value"), + ); text_in.set_string(inital_char.to_string()).unwrap(); Self { text: text_in, diff --git a/src/draw_buffer.rs b/src/draw_buffer.rs index c90b40c..afeeb5e 100644 --- a/src/draw_buffer.rs +++ b/src/draw_buffer.rs @@ -1,16 +1,16 @@ -use crate::palettes; +use crate::coordinates::FONT_SIZE16; use super::coordinates::{CharPosition, CharRect, FONT_SIZE, PixelRect, WINDOW_SIZE}; use font8x8::UnicodeFonts; pub struct DrawBuffer { - pub framebuffer: Box<[[u8; WINDOW_SIZE.0]; WINDOW_SIZE.1]>, + pub framebuffer: Box<[[u8; WINDOW_SIZE.0 as usize]; WINDOW_SIZE.1 as usize]>, } impl Default for DrawBuffer { fn default() -> Self { Self { - framebuffer: Box::new([[0; WINDOW_SIZE.0]; WINDOW_SIZE.1]), + framebuffer: Box::new([[0; WINDOW_SIZE.0 as usize]; WINDOW_SIZE.1 as usize]), } } } @@ -18,10 +18,8 @@ impl Default for DrawBuffer { impl DrawBuffer { pub const BACKGROUND_COLOR: u8 = 2; - pub fn new() -> Self { - Self { - framebuffer: Box::new([[0; WINDOW_SIZE.0]; WINDOW_SIZE.1]), - } + pub fn set(&mut self, y: u16, x: u16, color: u8) { + self.framebuffer[usize::from(y)][usize::from(x)] = color; } pub fn draw_char( @@ -31,15 +29,16 @@ impl DrawBuffer { fg_color: u8, bg_color: u8, ) { - // this is the top_left pixel - let position = (position.x() * FONT_SIZE, position.y() * FONT_SIZE); + let pixel_pos = PixelRect::from(CharRect::from(position)); + let position = (pixel_pos.left(), pixel_pos.top()); for (y, line) in char_data.iter().enumerate() { + let y = u16::try_from(y).unwrap(); for x in 0..8 { let color = match (line >> x) & 1 == 1 { true => fg_color, false => bg_color, }; - self.framebuffer[position.1 + y][position.0 + x] = color; + self.set(position.1 + y, position.0 + x, color); } } } @@ -54,7 +53,7 @@ impl DrawBuffer { for (num, char) in string.char_indices() { self.draw_char( font8x8::BASIC_FONTS.get(char).unwrap(), - position + (num, 0), + position + (u8::try_from(num).unwrap(), 0), fg_color, bg_color, ); @@ -67,12 +66,13 @@ impl DrawBuffer { &mut self, string: &str, position: CharPosition, - lenght: usize, + lenght: u8, fg_color: u8, bg_color: u8, ) { - if string.len() > lenght { - self.draw_string(&string[..=lenght], position, fg_color, bg_color) + let ulen = usize::from(lenght); + if string.len() > ulen { + self.draw_string(&string[..=ulen], position, fg_color, bg_color) } else { self.draw_string(string, position, fg_color, bg_color); self.draw_rect( @@ -80,7 +80,7 @@ impl DrawBuffer { CharRect::new( position.y(), position.y(), - position.x() + string.len(), + position.x() + u8::try_from(string.len()).unwrap(), position.x() + lenght, ), ); @@ -93,22 +93,23 @@ impl DrawBuffer { char_rect: CharRect, top_left_color: u8, bot_right_color: u8, - thickness: usize, + thickness: u8, ) { assert!(thickness <= FONT_SIZE); assert!(thickness > 0); let pixel_rect = PixelRect::from(char_rect); + let thickness = u16::from(thickness); for x in pixel_rect.left()..=pixel_rect.right() { for y in 0..thickness { - self.framebuffer[pixel_rect.top() + y][x] = top_left_color; - self.framebuffer[pixel_rect.bot() - y][x] = bot_right_color; + self.set(pixel_rect.top() + y, x, top_left_color); + self.set(pixel_rect.bot() - y, x, bot_right_color); } } for y in pixel_rect.top()..=pixel_rect.bot() { for x in 0..thickness { - self.framebuffer[y][pixel_rect.right() - x] = bot_right_color; - self.framebuffer[y][pixel_rect.left() + x] = top_left_color; + self.set(y, pixel_rect.right() - x, bot_right_color); + self.set(y, pixel_rect.left() + x, top_left_color); } } } @@ -119,45 +120,42 @@ impl DrawBuffer { background_color: u8, top_left_color: u8, bot_right_color: u8, - thickness: usize, + thickness: u8, ) { assert!(thickness < FONT_SIZE); assert!(thickness > 0); - // needs to be between 0 and FONT_SIZE - // const BOX_THICKNESS: usize = 1; - // const SPACE_FROM_BORDER: usize = FONT_SIZE - BOX_THICKNESS; - let space_from_border = FONT_SIZE - thickness; + let space_from_border = FONT_SIZE16 - u16::from(thickness); let pixel_rect = PixelRect::from(char_rect); // all pixel lines except those in top and bottom char line - for y in (pixel_rect.top() + FONT_SIZE)..=(pixel_rect.bot() - FONT_SIZE) { + for y in (pixel_rect.top() + FONT_SIZE16)..=(pixel_rect.bot() - FONT_SIZE16) { // left side foreground - for x in (pixel_rect.left() + space_from_border)..(pixel_rect.left() + FONT_SIZE) { - self.framebuffer[y][x] = top_left_color; + for x in (pixel_rect.left() + space_from_border)..(pixel_rect.left() + FONT_SIZE16) { + self.set(y, x, top_left_color); } // left side background for x in pixel_rect.left()..(pixel_rect.left() + space_from_border) { - self.framebuffer[y][x] = background_color; + self.set(y, x, background_color); } // need the plus ones, as the '..' would need to be exclusive on the low and inclusive on the high, which i dont know how to do for x in - (pixel_rect.right() - FONT_SIZE + 1)..(pixel_rect.right() - space_from_border + 1) + (pixel_rect.right() - FONT_SIZE16 + 1)..(pixel_rect.right() - space_from_border + 1) { - self.framebuffer[y][x] = bot_right_color; + self.set(y, x, bot_right_color); } // right side background for x in (pixel_rect.right() - space_from_border + 1)..=pixel_rect.right() { - self.framebuffer[y][x] = background_color; + self.set(y, x, background_color); } } // top char line - for y in pixel_rect.top()..(pixel_rect.top() + FONT_SIZE) { + for y in pixel_rect.top()..(pixel_rect.top() + FONT_SIZE16) { if y < pixel_rect.top() + space_from_border { for x in pixel_rect.horizontal_range() { - self.framebuffer[y][x] = background_color; + self.set(y, x, background_color); } } else { for x in pixel_rect.left()..=pixel_rect.right() { @@ -174,17 +172,17 @@ impl DrawBuffer { bot_right_color }; - self.framebuffer[y][x] = color; + self.set(y, x, color); } } } // bottom char line - for y in (pixel_rect.bot() - FONT_SIZE + 1)..=pixel_rect.bot() { + for y in (pixel_rect.bot() - FONT_SIZE16 + 1)..=pixel_rect.bot() { // does the top 'SPACE_FROM_BORDER' rows in background color if y > pixel_rect.bot() - space_from_border { for x in pixel_rect.horizontal_range() { - self.framebuffer[y][x] = background_color; + self.set(y, x, background_color); } } else { for x in pixel_rect.horizontal_range() { @@ -198,7 +196,8 @@ impl DrawBuffer { bot_right_color }; - self.framebuffer[y][x] = color; + // self.framebuffer[y][x] = color; + self.set(y, x, color); } } } @@ -210,19 +209,14 @@ impl DrawBuffer { } pub fn draw_pixel_rect(&mut self, color: u8, rect: PixelRect) { - for line in &mut self.framebuffer[rect.top()..=rect.bot()] { - line[rect.left()..=rect.right()].fill(color); + for line in &mut self.framebuffer[usize::from(rect.top())..=usize::from(rect.bot())] { + line[usize::from(rect.left())..=usize::from(rect.right())].fill(color); } } - /// for debugging. draws a pixel in the middle of the char - fn mark_char(&mut self, position: CharPosition) { - self.framebuffer[(position.y() + 4) * WINDOW_SIZE.0][position.x() + 4] = 1; - } - pub fn show_colors(&mut self) { - for i in 0..palettes::PALETTE_SIZE as u8 { - self.draw_rect(i, CharPosition::new(i as usize, 5).into()); + for i in 0..crate::render::palettes::PALETTE_SIZE as u8 { + self.draw_rect(i, CharPosition::new(i, 5).into()); } } } diff --git a/src/ui/header.rs b/src/header.rs similarity index 91% rename from src/ui/header.rs rename to src/header.rs index a6cd090..a7d1c5d 100644 --- a/src/ui/header.rs +++ b/src/header.rs @@ -2,8 +2,7 @@ use std::{io::Write, str::from_utf8}; use font8x8::UnicodeFonts; use torque_tracker_engine::{ - audio_processing::playback::PlaybackPosition, manager::PlaybackSettings, - project::pattern::Pattern, + PlaybackSettings, audio_processing::playback::PlaybackPosition, project::pattern::Pattern, }; use crate::{ @@ -145,7 +144,7 @@ impl Header { let char_color = if char.is_ascii_digit() { 3 } else { 0 }; draw_buffer.draw_char( font8x8::BASIC_FONTS.get(char).unwrap(), - CharPosition::new(2 + index, 9), + CharPosition::new(2 + u8::try_from(index).unwrap(), 9), char_color, 2, ); @@ -186,4 +185,18 @@ impl Header { // TODO: Not actually constant as it changes between Sample and Instrument mode buffer.draw_string("Sample", CharPosition::new(43, 3), 0, 2); } + + /// Returns the ID of the header root. This should be added as a child of the outer node + #[cfg(feature = "accesskit")] + pub fn build_tree( + &self, + tree: &mut Vec<(accesskit::NodeId, accesskit::Node)>, + ) -> accesskit::NodeId { + use accesskit::{Node, NodeId, Role}; + const HEADER_ID: NodeId = NodeId(1); + let root = Node::new(Role::Header); + + tree.push((HEADER_ID, root)); + HEADER_ID + } } diff --git a/src/main.rs b/src/main.rs index 4604bc7..1468a1b 100644 --- a/src/main.rs +++ b/src/main.rs @@ -1,12 +1,779 @@ -pub mod app; pub mod coordinates; +pub mod dialog; pub mod draw_buffer; -#[cfg(feature = "gpu_scaling")] -pub mod gpu; -pub mod palettes; +pub mod header; +pub mod pages; pub mod render; -pub mod ui; +pub mod widgets; + +use std::{ + collections::VecDeque, + fmt::Debug, + num::NonZero, + sync::{Arc, LazyLock, OnceLock}, + thread::JoinHandle, + time::Duration, +}; + +#[cfg(feature = "accesskit")] +use accesskit::NodeId; +use smol::{channel::Sender, lock::Mutex}; +use torque_tracker_engine::{ + AudioManager, OutputConfig, PlaybackSettings, ToWorkerMsg, + project::song::{Song, SongOperation}, +}; +use winit::{ + application::ApplicationHandler, + event::{Modifiers, WindowEvent}, + event_loop::{ActiveEventLoop, ControlFlow, EventLoopProxy}, + keyboard::{Key, NamedKey}, + window::WindowAttributes, +}; + +use cpal::{ + BufferSize, OutputStreamTimestamp, SupportedBufferSize, + traits::{DeviceTrait, HostTrait}, +}; + +use pages::{ + AllPages, PageEvent, PageResponse, PagesEnum, order_list::OrderListPageEvent, + pattern::PatternPageEvent, +}; + +use crate::dialog::DialogEnum; + +use { + dialog::{DialogManager, DialogResponse, confirm::ConfirmDialog, page_menu::MainMenu}, + draw_buffer::DrawBuffer, + header::{Header, HeaderEvent}, +}; + +#[cfg(all(feature = "gpu_scaling", feature = "soft_scaling"))] +use render::BothRenderBackend as RenderBackend; +#[cfg(all(feature = "gpu_scaling", not(feature = "soft_scaling")))] +use render::GPURenderBackend as RenderBackend; +#[cfg(all(not(feature = "gpu_scaling"), feature = "soft_scaling"))] +use render::SoftRenderBackend as RenderBackend; + +pub static EXECUTOR: smol::Executor = smol::Executor::new(); +/// Song data +pub static SONG_MANAGER: LazyLock> = + LazyLock::new(|| Mutex::new(AudioManager::new(Song::default()))); +/// Sender for Song changes +pub static SONG_OP_SEND: OnceLock> = OnceLock::new(); + +/// shorter function name +pub fn send_song_op(op: SongOperation) { + SONG_OP_SEND.get().unwrap().send_blocking(op).unwrap(); +} + +pub enum GlobalEvent { + OpenDialog(DialogEnum), + Page(PageEvent), + Header(HeaderEvent), + /// also closes all dialogs + GoToPage(PagesEnum), + // Needed because only in the main app i know which pattern is selected, so i know what to play + Playback(PlaybackType), + #[cfg(feature = "accesskit")] + Accesskit(accesskit_winit::WindowEvent), + CloseRequested, + CloseApp, + ConstRedraw, +} + +impl Debug for GlobalEvent { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + let mut debug = f.debug_struct("GlobalEvent"); + match self { + GlobalEvent::OpenDialog(_) => debug.field("OpenDialog", &"closure"), + GlobalEvent::Page(page_event) => debug.field("Page", page_event), + GlobalEvent::Header(header_event) => debug.field("Header", header_event), + GlobalEvent::GoToPage(pages_enum) => debug.field("GoToPage", pages_enum), + GlobalEvent::CloseRequested => debug.field("CloseRequested", &""), + GlobalEvent::CloseApp => debug.field("CloseApp", &""), + GlobalEvent::ConstRedraw => debug.field("ConstRedraw", &""), + GlobalEvent::Playback(playback_type) => debug.field("Playback", &playback_type), + #[cfg(feature = "accesskit")] + GlobalEvent::Accesskit(window_event) => debug.field("Accesskit", &window_event), + }; + debug.finish() + } +} + +#[cfg(feature = "accesskit")] +impl From for GlobalEvent { + fn from(value: accesskit_winit::Event) -> Self { + // ignore window id, because i only have one window + Self::Accesskit(value.window_event) + } +} + +#[derive(Clone, Copy, Debug)] +pub enum PlaybackType { + Stop, + Song, + Pattern, + FromOrder, + FromCursor, +} + +struct WorkerThreads { + handles: [JoinHandle<()>; 2], + close_msg: [Sender<()>; 2], +} + +impl WorkerThreads { + fn new() -> Self { + let (send1, recv1) = smol::channel::unbounded(); + let thread1 = std::thread::Builder::new() + .name("Background Worker 1".into()) + .spawn(Self::worker_task(recv1)) + .unwrap(); + let (send2, recv2) = smol::channel::unbounded(); + let thread2 = std::thread::Builder::new() + .name("Background Worker 2".into()) + .spawn(Self::worker_task(recv2)) + .unwrap(); + + Self { + handles: [thread1, thread2], + close_msg: [send1, send2], + } + } + + fn worker_task(recv: smol::channel::Receiver<()>) -> impl FnOnce() + Send + 'static { + move || { + smol::block_on(EXECUTOR.run(async { recv.recv().await.unwrap() })); + } + } + + /// prepares the closing of the threads by signalling them to stop + fn send_close(&mut self) { + _ = self.close_msg[0].send_blocking(()); + _ = self.close_msg[1].send_blocking(()); + } + + fn close_all(mut self) { + self.send_close(); + let [handle1, handle2] = self.handles; + handle1.join().unwrap(); + handle2.join().unwrap(); + } +} + +pub struct EventQueue<'a>(&'a mut VecDeque); + +impl EventQueue<'_> { + pub fn push(&mut self, event: GlobalEvent) { + self.0.push_back(event); + } +} + +/// window with all the additional stuff +struct Window { + window: Arc, + render_backend: RenderBackend, + #[cfg(feature = "accesskit")] + adapter: (accesskit_winit::Adapter, accesskit::Affine), +} + +pub struct App { + window: Option, + draw_buffer: DrawBuffer, + modifiers: Modifiers, + ui_pages: AllPages, + event_queue: VecDeque, + dialog_manager: DialogManager, + header: Header, + event_loop_proxy: EventLoopProxy, + worker_threads: Option, + audio_stream: Option<( + cpal::Stream, + smol::Task<()>, + torque_tracker_engine::StreamSend, + )>, +} + +impl ApplicationHandler for App { + fn new_events(&mut self, _: &ActiveEventLoop, start_cause: winit::event::StartCause) { + if start_cause == winit::event::StartCause::Init { + LazyLock::force(&SONG_MANAGER); + self.worker_threads = Some(WorkerThreads::new()); + let (send, recv) = smol::channel::unbounded(); + SONG_OP_SEND.get_or_init(|| send); + EXECUTOR + .spawn(async move { + while let Ok(op) = recv.recv().await { + let mut manager = SONG_MANAGER.lock().await; + // if there is no active channel the buffer isn't used, so it doesn't matter that it's wrong + let buffer_time = manager.last_buffer_time(); + // spin loop to lock the song + let mut song = loop { + if let Some(song) = manager.try_edit_song() { + break song; + } + // smol mutex lock is held across await point + smol::Timer::after(buffer_time).await; + }; + // apply the received op + song.apply_operation(op).unwrap(); + // try to get more ops. This avoids repeated locking of the song when a lot of operations are + // in queue + while let Ok(op) = recv.try_recv() { + song.apply_operation(op).unwrap(); + } + drop(song); + } + }) + .detach(); + // spawn a task to collect sample garbage every 10 seconds + EXECUTOR + .spawn(async { + loop { + let mut lock = SONG_MANAGER.lock().await; + lock.collect_garbage(); + drop(lock); + smol::Timer::after(Duration::from_secs(10)).await; + } + }) + .detach(); + self.start_audio_stream(); + } + } + + fn resumed(&mut self, event_loop: &winit::event_loop::ActiveEventLoop) { + self.build_window(event_loop); + } + + fn suspended(&mut self, _: &ActiveEventLoop) { + // my window and GPU state have been invalidated + self.window = None; + } + + fn window_event( + &mut self, + event_loop: &winit::event_loop::ActiveEventLoop, + _: winit::window::WindowId, + event: WindowEvent, + ) { + // destructure so i don't have to always type self. + let Self { + window, + draw_buffer, + modifiers, + ui_pages, + event_queue, + dialog_manager, + header, + event_loop_proxy: _, + worker_threads: _, + audio_stream: _, + } = self; + + // i won't get a window_event without having a window, so unwrap is fine + let window = window.as_mut().unwrap(); + #[cfg(not(feature = "accesskit"))] + let Window { + window, + render_backend, + } = window; + #[cfg(feature = "accesskit")] + let Window { + window, + render_backend, + adapter, + } = window; + // don't want window to be mutable + let window = window.as_ref(); + + #[cfg(feature = "accesskit")] + adapter.0.process_event(window, &event); + // limit the pages and widgets to only push events and not read or pop + let event_queue = &mut EventQueue(event_queue); + + match event { + WindowEvent::CloseRequested => Self::close_requested(event_queue), + WindowEvent::Resized(physical_size) => { + render_backend.resize(physical_size); + window.request_redraw(); + } + WindowEvent::ScaleFactorChanged { + scale_factor: _, + inner_size_writer: _, + } => { + // window_state.resize(**new_inner_size); + // due to a version bump in winit i dont know anymore how to handle this event so i just ignore it for know and see if it makes problems in the future + // i have yet only received this event on linux wayland, not macos + println!("Window Scale Factor Changed"); + } + WindowEvent::RedrawRequested => { + // draw the new frame buffer + // TODO: split redraw header and redraw page. As soon as header gets a spectrometer this becomes important + header.draw(draw_buffer); + ui_pages.draw(draw_buffer); + dialog_manager.draw(draw_buffer); + #[cfg(feature = "accesskit")] + adapter.0.update_if_active(|| { + Self::produce_full_tree(ui_pages, header, dialog_manager, adapter.1) + }); + // notify the windowing system that drawing is done and the new buffer is about to be pushed + window.pre_present_notify(); + // push the framebuffer into GPU/softbuffer and render it onto the screen + render_backend.render(&draw_buffer.framebuffer, event_loop); + } + WindowEvent::KeyboardInput { + device_id: _, + event, + is_synthetic, + } => { + if is_synthetic { + return; + } + + if event.state.is_pressed() { + if event.logical_key == Key::Named(NamedKey::F5) { + self.event_queue + .push_back(GlobalEvent::Playback(PlaybackType::Song)); + return; + } else if event.logical_key == Key::Named(NamedKey::F6) { + if modifiers.state().shift_key() { + self.event_queue + .push_back(GlobalEvent::Playback(PlaybackType::FromOrder)); + } else { + self.event_queue + .push_back(GlobalEvent::Playback(PlaybackType::Pattern)); + } + return; + } else if event.logical_key == Key::Named(NamedKey::F8) { + self.event_queue + .push_back(GlobalEvent::Playback(PlaybackType::Stop)); + return; + } + } + // key_event didn't start or stop the song, so process normally + if let Some(dialog) = dialog_manager.active_dialog_mut() { + match dialog.process_input(&event, modifiers, event_queue) { + DialogResponse::Close => { + dialog_manager.close_dialog(); + // if i close a pop_up i need to redraw the const part of the page as the pop-up overlapped it probably + ui_pages.request_draw_const(); + window.request_redraw(); + } + DialogResponse::RequestRedraw => window.request_redraw(), + DialogResponse::None => (), + } + } else { + if event.state.is_pressed() && event.logical_key == Key::Named(NamedKey::Escape) + { + event_queue.push(GlobalEvent::OpenDialog(dialog::DialogEnum::Main( + MainMenu::new(), + ))); + } + + match ui_pages.process_key_event(&self.modifiers, &event, event_queue) { + PageResponse::RequestRedraw => window.request_redraw(), + PageResponse::None => (), + } + } + } + // not sure if i need it just to make sure i always have all current modifiers to be used with keyboard events + WindowEvent::ModifiersChanged(new_modifiers) => *modifiers = new_modifiers, + + _ => (), + } + + while let Some(event) = self.event_queue.pop_front() { + self.user_event(event_loop, event); + } + } + + /// i may need to add the ability for events to add events to the event queue, but that should be possible + fn user_event(&mut self, event_loop: &ActiveEventLoop, event: GlobalEvent) { + let event_queue = &mut EventQueue(&mut self.event_queue); + match event { + GlobalEvent::OpenDialog(dialog) => { + self.dialog_manager.open_dialog(dialog); + _ = self.try_request_redraw(); + } + GlobalEvent::Page(c) => match self.ui_pages.process_page_event(c, event_queue) { + PageResponse::RequestRedraw => _ = self.try_request_redraw(), + PageResponse::None => (), + }, + GlobalEvent::Header(header_event) => { + self.header.process_event(header_event); + _ = self.try_request_redraw(); + } + GlobalEvent::GoToPage(pages_enum) => { + self.dialog_manager.close_all(); + self.ui_pages.switch_page(pages_enum); + _ = self.try_request_redraw(); + } + GlobalEvent::CloseApp => event_loop.exit(), + GlobalEvent::CloseRequested => Self::close_requested(event_queue), + GlobalEvent::ConstRedraw => { + self.ui_pages.request_draw_const(); + _ = self.try_request_redraw(); + } + GlobalEvent::Playback(playback_type) => { + let msg = match playback_type { + PlaybackType::Song => Some(ToWorkerMsg::Playback(PlaybackSettings::Order { + idx: 0, + should_loop: true, + })), + PlaybackType::Stop => Some(ToWorkerMsg::StopPlayback), + PlaybackType::Pattern => { + Some(ToWorkerMsg::Playback(self.header.play_current_pattern())) + } + PlaybackType::FromOrder => { + Some(ToWorkerMsg::Playback(self.header.play_current_order())) + } + PlaybackType::FromCursor => None, + }; + + if let Some(msg) = msg { + self.audio_stream + .as_mut() + .expect( + "audio stream should always be active, should still handle this error", + ) + .2 + .try_msg_worker(msg) + .expect("buffer full. either increase size or retry somehow") + } + } + #[cfg(feature = "accesskit")] + GlobalEvent::Accesskit(window_event) => { + use accesskit_winit::WindowEvent; + match window_event { + WindowEvent::InitialTreeRequested => { + // there probably should always be a window + // make sure we always respond to this event. I hope it can't be send when there is no window + let window = self.window.as_mut().unwrap(); + window.adapter.0.update_if_active(|| { + Self::produce_full_tree( + &self.ui_pages, + &self.header, + &self.dialog_manager, + window.adapter.1, + ) + }); + } + WindowEvent::ActionRequested(event) => { + match event.target.0 / Self::ID_LEVEL_1 { + Self::HEADER_ID => todo!("decide if the header should be interactive"), + Self::PAGE_ID => { + let event = { + let mut event = event; + // information about lvl 1 was processed, so remove it + event.target.0 -= Self::PAGE_ID * Self::ID_LEVEL_1; + event + }; + match self.ui_pages.process_accesskit_event(event) { + PageResponse::RequestRedraw => { + self.try_request_redraw().unwrap() + } + PageResponse::None => (), + } + } + Self::DIALOG_ID => (), + // should i ignore invalid IDs? probably not as that would be a bug either in accesskit or my code + _ => unreachable!(), + } + } // i don't have any extra state for accessability so i don't need to cleanup anything + WindowEvent::AccessibilityDeactivated => (), + } + } + } + } + + fn exiting(&mut self, _: &ActiveEventLoop) { + if let Some(workers) = self.worker_threads.take() { + // wait for all the threads to close + workers.close_all(); + } + if self.audio_stream.is_some() { + self.close_audio_stream(); + } + } +} + +impl App { + pub fn new(proxy: EventLoopProxy) -> Self { + Self { + window: None, + draw_buffer: DrawBuffer::default(), + modifiers: Modifiers::default(), + ui_pages: AllPages::new(proxy.clone()), + dialog_manager: DialogManager::new(), + header: Header::default(), + event_loop_proxy: proxy, + worker_threads: None, + audio_stream: None, + event_queue: VecDeque::with_capacity(3), + } + } + + // TODO: should this be its own function?? or is there something better + fn close_requested(events: &mut EventQueue<'_>) { + events.push(GlobalEvent::OpenDialog(DialogEnum::Confirm( + ConfirmDialog::new( + "Close Torque Tracker?", + Some(|| GlobalEvent::CloseApp), + None, + ), + ))); + } + + /// tries to request a redraw. if there currently is no window this fails + fn try_request_redraw(&self) -> Result<(), ()> { + if let Some(window) = &self.window { + window.window.request_redraw(); + Ok(()) + } else { + Err(()) + } + } + + fn build_window(&mut self, event_loop: &ActiveEventLoop) { + self.window.get_or_insert_with(|| { + let mut attributes = WindowAttributes::default(); + attributes.active = true; + attributes.resizable = true; + attributes.resize_increments = None; + attributes.title = String::from("Torque Tracker"); + // for accesskit + attributes.visible = false; + + let window = Arc::new(event_loop.create_window(attributes).unwrap()); + let render_backend = + RenderBackend::new(window.clone(), render::palettes::Palette::CAMOUFLAGE); + + #[cfg(feature = "accesskit")] + let adapter = { + accesskit_winit::Adapter::with_event_loop_proxy( + event_loop, + &window, + self.event_loop_proxy.clone(), + ) + }; + + window.set_visible(true); + let size = render_backend.get_size(); + Window { + window, + render_backend, + #[cfg(feature = "accesskit")] + adapter: (adapter, create_transform(size)), + } + }); + } + + // TODO: make this configurable + fn start_audio_stream(&mut self) { + use cpal::StreamInstant; + + assert!(self.audio_stream.is_none()); + let host = cpal::default_host(); + let device = host.default_output_device().unwrap(); + let default_config = device.default_output_config().unwrap(); + let (config, buffer_size) = { + let mut config = default_config.config(); + let buffer_size = { + let default = default_config.buffer_size(); + match default { + SupportedBufferSize::Unknown => 1024, + SupportedBufferSize::Range { min, max } => u32::min(u32::max(1024, *min), *max), + } + }; + config.buffer_size = BufferSize::Fixed(buffer_size); + (config, buffer_size) + }; + let mut guard = SONG_MANAGER.lock_blocking(); + let (mut worker, buffer_time, status, stream_send) = guard + .get_callback::( + OutputConfig { + buffer_size, + channel_count: NonZero::new(config.channels).unwrap(), + sample_rate: NonZero::new(config.sample_rate.0).unwrap(), + interpolation: torque_tracker_engine::audio_processing::Interpolation::Linear, + }, + cpal::OutputStreamTimestamp { + callback: cpal::StreamInstant::new(0, 0), + playback: cpal::StreamInstant::new(0, 0), + }, + ); + // keep the guard as short as possible to not block the async threads + drop(guard); + let stream = device + .build_output_stream( + &config, + move |data, info| { + worker(data, info.timestamp()); + }, + |err| eprintln!("audio stream err: {err:?}"), + None, + ) + .unwrap(); + // spawn a task to process the audio playback status updates + let proxy = self.event_loop_proxy.clone(); + let task = EXECUTOR.spawn(async move { + let buffer_time = buffer_time; + let mut status_recv = status; + // maybe also send the timestamp every second or so + let mut old_playback = None; + let mut old_timestamp = OutputStreamTimestamp { + callback: StreamInstant::new(0, 0), + playback: StreamInstant::new(0, 0), + }; + loop { + smol::Timer::after(buffer_time).await; + let Some(read) = status_recv.try_get() else { + // we had a lock for way too long, so now we are behing and have to wait for the writer to finish + // writing once. Then locking will succeed again. + continue; + }; + let playback = read.0; + let timestamp = read.1; + // only react on status changes. could at some point be made more granular + if playback != old_playback { + old_playback = playback; + // println!("playback status: {status:?}"); + let pos = playback.map(|s| s.position); + proxy + .send_event(GlobalEvent::Header(HeaderEvent::SetPlayback(pos))) + .unwrap(); + let pos = playback.map(|s| (s.position.pattern, s.position.row)); + proxy + .send_event(GlobalEvent::Page(PageEvent::Pattern( + PatternPageEvent::PlaybackPosition(pos), + ))) + .unwrap(); + // does a map flatten. idk why it's called and_then + let pos = playback.and_then(|s| s.position.order); + proxy + .send_event(GlobalEvent::Page(PageEvent::OrderList( + OrderListPageEvent::SetPlayback(pos), + ))) + .unwrap(); + } + + // also check for changed timestamp + if timestamp != old_timestamp { + old_timestamp = timestamp; + // maybe at some point use the data + } + } + }); + self.audio_stream = Some((stream, task, stream_send)); + } + + fn close_audio_stream(&mut self) { + let (stream, task, mut stream_send) = self.audio_stream.take().unwrap(); + // stop playback + _ = stream_send.try_msg_worker(ToWorkerMsg::StopPlayback); + _ = stream_send.try_msg_worker(ToWorkerMsg::StopLiveNote); + // kill the task. using `cancel` doesn't make sense because it doesn't finishe anyways + drop(task); + // lastly kill the audio stream + drop(stream); + } +} + +#[cfg(feature = "accesskit")] +impl App { + // Node ID Layout: + // xxx_xxx_xxx_xxx_xxx + // In Widget lvl 4 + // In Page/Dialog/Header lvl 3 + // Which Page/Header/Dialog lvl 2 + // Page, Dialog or Header lvl 1 + // Root + const ROOT_ID: NodeId = NodeId(1_000_000_000_000); + pub const ID_LEVEL_1: u64 = 1_000_000_000; + pub const ID_LEVEL_2: u64 = 1_000_000; + pub const ID_LEVEL_3: u64 = 1_000; + pub const ID_LEVEL_4: u64 = 1; // do i need this? + + pub const HEADER_ID: u64 = 1; + pub const PAGE_ID: u64 = 2; + pub const DIALOG_ID: u64 = 3; + + fn produce_full_tree( + pages: &AllPages, + header: &Header, + dialogs: &DialogManager, + transform: accesskit::Affine, + ) -> accesskit::TreeUpdate { + use accesskit::TreeUpdate; + use accesskit::{Node, Role, Tree}; + + let tree = Tree { + root: Self::ROOT_ID, + toolkit_name: Some(String::from("Torque Tracker Custom")), + toolkit_version: None, + }; + let mut root_node = Node::new(Role::Window); + let mut nodes = Vec::new(); + root_node.set_label("Torque Tracker"); + root_node.set_language("English"); + let header_id = header.build_tree(&mut nodes); + root_node.push_child(header_id); + root_node.set_transform(transform); + + let resp = pages.build_tree(&mut nodes); + let mut focused = resp.selected; + root_node.push_child(resp.root); + + if let Some(dialog) = dialogs.active_dialog() { + let resp = dialog.build_tree(&mut nodes); + root_node.push_child(resp.root); + focused = resp.selected; + } + + nodes.push((Self::ROOT_ID, root_node)); + TreeUpdate { + nodes, + tree: Some(tree), + focus: focused, + } + } + + fn process_accesskit_event(&mut self, event: accesskit::ActionRequest) {} +} + +impl Drop for App { + fn drop(&mut self) { + if self.audio_stream.is_some() { + self.close_audio_stream(); + } + } +} + +#[cfg(feature = "accesskit")] +pub struct AccessResponse { + pub root: accesskit::NodeId, + pub selected: accesskit::NodeId, +} + +#[cfg(feature = "accesskit")] +fn create_transform(size: winit::dpi::PhysicalSize) -> accesskit::Affine { + accesskit::Affine::scale_non_uniform( + size.width as f64 / crate::coordinates::WINDOW_SIZE_CHARS.0 as f64, + size.height as f64 / crate::coordinates::WINDOW_SIZE_CHARS.1 as f64, + ) +} fn main() { - app::run(); + let event_loop = winit::event_loop::EventLoop::::with_user_event() + .build() + .unwrap(); + event_loop.set_control_flow(ControlFlow::Wait); + // i don't need any raw device events. Keyboard and Mouse coming as window events are enough + event_loop.listen_device_events(winit::event_loop::DeviceEvents::Never); + let event_loop_proxy = event_loop.create_proxy(); + let mut app = App::new(event_loop_proxy); + app.header.draw_constant(&mut app.draw_buffer); + + event_loop.run_app(&mut app).unwrap(); } diff --git a/src/ui/pages/help_page.rs b/src/pages/help_page.rs similarity index 68% rename from src/ui/pages/help_page.rs rename to src/pages/help_page.rs index 195c56f..72a711d 100644 --- a/src/ui/pages/help_page.rs +++ b/src/pages/help_page.rs @@ -1,4 +1,4 @@ -use crate::{app::EventQueue, coordinates::CharRect, draw_buffer::DrawBuffer}; +use crate::{EventQueue, coordinates::CharRect, draw_buffer::DrawBuffer}; use super::{Page, PageResponse}; @@ -19,6 +19,14 @@ impl Page for HelpPage { ) -> PageResponse { PageResponse::None } + + #[cfg(feature = "accesskit")] + fn build_tree( + &self, + tree: &mut Vec<(accesskit::NodeId, accesskit::Node)>, + ) -> crate::AccessResponse { + todo!() + } } impl HelpPage { diff --git a/src/ui/pages.rs b/src/pages/mod.rs similarity index 70% rename from src/ui/pages.rs rename to src/pages/mod.rs index 10b234a..aeb3c1e 100644 --- a/src/ui/pages.rs +++ b/src/pages/mod.rs @@ -16,10 +16,10 @@ use winit::{ }; use crate::{ - app::{EventQueue, GlobalEvent}, + EventQueue, GlobalEvent, coordinates::{CharPosition, CharRect, WINDOW_SIZE_CHARS}, draw_buffer::DrawBuffer, - ui::pages::sample_list::SampleListEvent, + pages::sample_list::SampleListEvent, }; pub trait Page { @@ -33,83 +33,14 @@ pub trait Page { // please give me reborrowing for custom structs rustc :3 events: &mut EventQueue<'_>, ) -> PageResponse; -} - -/// creates a struct called WidgetList with all the specified fields. -/// -/// inserts a const index for every field in WidgetList into the specified struct -/// as well as a function to query for the Widgets from a those indices. -/// -/// Needs at least one fields to work. If it is less, just write from hand. -macro_rules! create_widget_list { - (@function rsp: $response:ty; $($name:ident),*) => { - fn get_widget_mut(&mut self, idx: usize) -> &mut dyn crate::ui::widgets::Widget { - paste::paste! ( - $(if idx == Self::[<$name:upper>] { &mut self.$name } else)* - { panic!("invalid index {:?}", idx) } - ) - } - fn get_widget(&self, idx: usize) -> &dyn crate::ui::widgets::Widget { - paste::paste! ( - $(if idx == Self::[<$name:upper>] { &self.$name } else)* - { panic!("invalid index {:?}", idx) } - ) - } - }; - // inital with more than one name - (response: $response:ty; $list_name: ident { $name:ident: $type:ty, $($n:ident: $t:ty),* }) => ( - struct $list_name { - selected: usize, - $name: $type, - $($n: $t),* - } - impl $list_name { - paste::paste!( - const [<$name:upper>]: usize = 0; - ); - const INDEX_RANGE: std::ops::Range = 0..Self::WIDGET_COUNT; - - pub fn draw_widgets(&self, draw_buffer: &mut crate::draw_buffer::DrawBuffer) { - for widget in Self::INDEX_RANGE { - let is_selected = widget == self.selected; - self.get_widget(widget).draw(draw_buffer, is_selected); - } - } - - pub fn process_input( - &mut self, - key_event: &winit::event::KeyEvent, - modifiers: &winit::event::Modifiers, - events: &mut crate::app::EventQueue<'_>, - ) -> WidgetResponse<$response> { - self.get_widget_mut(self.selected).process_input(modifiers, key_event, events) - } - - crate::ui::pages::create_widget_list!($($n),* ; $name); - crate::ui::pages::create_widget_list!(@function rsp: $response; $name, $($n),*); - } - ); - // last name - ($name:ident ; $prev:ident) => ( - // const $name: usize = $num; - paste::paste!( - const [<$name:upper>]: usize = Self::[<$prev:upper>] + 1usize; - const WIDGET_COUNT: usize = Self::[<$name:upper>] + 1usize; - ); - ); - // loop over names - ($name:ident, $($n:ident),+ ; $prev:ident) => ( - // const $name: usize = $num; - paste::paste!( - const [<$name:upper>]: usize = Self::[<$prev:upper>] + 1; - ); - crate::ui::pages::create_widget_list!($($n),+ ; $name); - ); + #[cfg(feature = "accesskit")] + fn build_tree( + &self, + tree: &mut Vec<(accesskit::NodeId, accesskit::Node)>, + ) -> crate::AccessResponse; } -pub(crate) use create_widget_list; - pub enum PageResponse { RequestRedraw, None, @@ -229,19 +160,20 @@ impl AllPages { pub fn draw_constant(&mut self, draw_buffer: &mut DrawBuffer) { // draw page title let title = self.get_title(); + let title_len = u8::try_from(title.len()).expect("title doesn't fit on the page"); let middle = WINDOW_SIZE_CHARS.0 / 2; - let str_start = middle - (title.len() / 2); + let str_start = middle - (title_len / 2); draw_buffer.draw_string(title, CharPosition::new(str_start, 11), 0, 2); const DOTTED: [u8; 8] = [0, 0, 0, 0b01010101, 0, 0, 0, 0]; draw_buffer.draw_rect(2, CharRect::new(11, 11, str_start - 1, str_start - 1)); draw_buffer.draw_rect( 2, - CharRect::new(11, 11, str_start + title.len(), str_start + title.len()), + CharRect::new(11, 11, str_start + title_len, str_start + title_len), ); for x in 1..=(str_start - 2) { draw_buffer.draw_char(DOTTED, CharPosition::new(x, 11), 1, 2); } - for x in (str_start + title.len() + 1)..=(WINDOW_SIZE_CHARS.0 - 2) { + for x in (str_start + title_len + 1)..=(WINDOW_SIZE_CHARS.0 - 2) { draw_buffer.draw_char(DOTTED, CharPosition::new(x, 11), 1, 2); } // draw page const @@ -305,4 +237,17 @@ impl AllPages { PageResponse::None } } + + #[cfg(feature = "accesskit")] + pub fn build_tree( + &self, + tree: &mut Vec<(accesskit::NodeId, accesskit::Node)>, + ) -> crate::AccessResponse { + self.get_page().build_tree(tree) + } + + #[cfg(feature = "accesskit")] + pub fn process_accesskit_event(&mut self, event: accesskit::ActionRequest) -> PageResponse { + todo!() + } } diff --git a/src/ui/pages/order_list.rs b/src/pages/order_list.rs similarity index 84% rename from src/ui/pages/order_list.rs rename to src/pages/order_list.rs index 441a1f7..246d23f 100644 --- a/src/ui/pages/order_list.rs +++ b/src/pages/order_list.rs @@ -1,25 +1,24 @@ use std::str::from_utf8; use std::{array, io::Write}; -use torque_tracker_engine::channel::Pan; -use torque_tracker_engine::project::song::SongOperation; +use torque_tracker_engine::project::song::{Pan, SongOperation}; use torque_tracker_engine::{file::impulse_format::header::PatternOrder, project::song::Song}; use winit::keyboard::{Key, ModifiersState, NamedKey}; -use crate::app::{EventQueue, GlobalEvent, send_song_op}; -use crate::ui::header::HeaderEvent; -use crate::ui::widgets::{NextWidget, StandardResponse, Widget}; +use crate::header::HeaderEvent; +use crate::widgets::{NextWidget, Widget, WidgetResponse}; +use crate::{EventQueue, GlobalEvent, send_song_op}; use crate::{ coordinates::{CharPosition, CharRect}, - ui::widgets::slider::Slider, + widgets::slider::Slider, }; use super::{Page, PageEvent, PageResponse}; #[derive(Debug, Clone)] pub enum OrderListPageEvent { - SetVolumeCurrent(i16), - SetPanCurrent(i16), + SetVolumeCurrent(u8), + SetPanCurrent(u8), SetPlayback(Option), } @@ -50,11 +49,13 @@ pub struct OrderListPage { order_draw: u16, order_playback: Option, pattern_order: [PatternOrder; Song::MAX_ORDERS], - volume: [Slider<0, 64, ()>; 64], - pan: [Slider<0, 64, ()>; 64], + volume: [Slider<0, 64>; 64], + pan: [Slider<0, 64>; 64], } impl OrderListPage { + const PAGE_ID: u64 = 11_000_000_000; + pub fn new() -> Self { Self { cursor: Cursor::Order, @@ -64,12 +65,12 @@ impl OrderListPage { pattern_order: [PatternOrder::EndOfSong; Song::MAX_ORDERS], order_playback: None, volume: array::from_fn(|idx| { + let idx = u8::try_from(idx).unwrap(); let pos = if idx >= 32 { CharPosition::new(65, 15 + idx - 32) } else { CharPosition::new(31, 15 + idx) }; - let idx = u8::try_from(idx).unwrap(); Slider::new( 64, pos, @@ -77,23 +78,23 @@ impl OrderListPage { NextWidget::default(), |value| { GlobalEvent::Page(PageEvent::OrderList( - OrderListPageEvent::SetVolumeCurrent(value), + OrderListPageEvent::SetVolumeCurrent(value.try_into().unwrap()), )) }, - move |vol| { - // impossible to trigger as long as the slider is correct - let vol = u8::try_from(vol).unwrap(); - send_song_op(SongOperation::SetVolume(idx, vol)); - }, + #[cfg(feature = "accesskit")] + ( + accesskit::NodeId(Self::PAGE_ID + u64::from(idx)), + format!("Volume Channel {idx}").into_boxed_str(), + ), ) }), pan: array::from_fn(|idx| { + let idx = u8::try_from(idx).unwrap(); let pos = if idx >= 32 { CharPosition::new(65, 15 + idx - 32) } else { CharPosition::new(31, 15 + idx) }; - let idx = u8::try_from(idx).unwrap(); Slider::new( 32, pos, @@ -101,15 +102,14 @@ impl OrderListPage { NextWidget::default(), |value| { GlobalEvent::Page(PageEvent::OrderList(OrderListPageEvent::SetPanCurrent( - value, + u8::try_from(value).unwrap(), ))) }, - move |pan| { - // surround and disabled not supported yet - // This panic can't be triggered as long as the slider is correct. - let pan = Pan::Value(u8::try_from(pan).unwrap()); - send_song_op(SongOperation::SetPan(idx, pan)); - }, + #[cfg(feature = "accesskit")] + ( + accesskit::NodeId(Self::PAGE_ID + u64::from(idx)), + format!("Pan Channel {idx}").into_boxed_str(), + ), ) }), } @@ -125,8 +125,9 @@ impl OrderListPage { Cursor::VolPan(c) => c, }; self.volume[usize::from(cursor - 1)] - .try_set(vol) + .try_set(i16::from(vol)) .expect("the value was created from the slider, so it has to fit."); + send_song_op(SongOperation::SetVolume(cursor - 1, vol)); } OrderListPageEvent::SetPanCurrent(pan) => { let cursor = match self.cursor { @@ -136,8 +137,9 @@ impl OrderListPage { Cursor::VolPan(c) => c, }; self.pan[usize::from(cursor - 1)] - .try_set(pan) - .expect("the event was created from the slider, so has to fit.") + .try_set(i16::from(pan)) + .expect("the event was created from the slider, so has to fit."); + send_song_op(SongOperation::SetPan(cursor - 1, Pan::Value(pan))); } OrderListPageEvent::SetPlayback(o) => self.order_playback = o, }; @@ -230,6 +232,7 @@ impl Page for OrderListPage { const ORDER_BASE_POS: CharPosition = CharPosition::new(2, 15); let mut buf = [0; 3]; for (pos, order) in (self.order_draw..=self.order_draw + 31).enumerate() { + let pos = u8::try_from(pos).unwrap(); // row index let mut curse: std::io::Cursor<&mut [u8]> = std::io::Cursor::new(&mut buf); write!(&mut curse, "{:03}", order).unwrap(); @@ -303,8 +306,8 @@ impl Page for OrderListPage { .unwrap(), ORDER_BASE_POS + CharPosition::new( - 4 + usize::from(self.order_cursor.digit), - usize::from(self.order_cursor.order - self.order_draw), + 4 + self.order_cursor.digit, + u8::try_from(self.order_cursor.order - self.order_draw).unwrap(), ), 0, 3, @@ -315,9 +318,9 @@ impl Page for OrderListPage { let mut curse: std::io::Cursor<&mut [u8]> = std::io::Cursor::new(&mut buf); write!(&mut curse, "{:02}", c).unwrap(); let pos = if c <= 32 { - CHANNEL_BASE_LEFT + CharPosition::new(0, usize::from(c - 1)) + CHANNEL_BASE_LEFT + CharPosition::new(0, c - 1) } else { - CHANNEL_BASE_RIGHT + CharPosition::new(0, usize::from(c - 33)) + CHANNEL_BASE_RIGHT + CharPosition::new(0, c - 33) }; draw_buffer.draw_string(CHANNEL, pos, 3, 2); draw_buffer.draw_string( @@ -522,25 +525,51 @@ impl Page for OrderListPage { } match self.mode { - Mode::Panning => match self.pan[usize::from(c - 1)] - .process_input(modifiers, key_event, events) - .standard - { - StandardResponse::SwitchFocus(_) => return PageResponse::None, - StandardResponse::RequestRedraw => return PageResponse::RequestRedraw, - StandardResponse::None => return PageResponse::None, - }, - Mode::Volume => match self.volume[usize::from(c - 1)] - .process_input(modifiers, key_event, events) - .standard - { - StandardResponse::SwitchFocus(_) => return PageResponse::None, - StandardResponse::RequestRedraw => return PageResponse::RequestRedraw, - StandardResponse::None => return PageResponse::None, - }, + Mode::Panning => { + let slider = &mut self.pan[usize::from(c - 1)]; + match slider.process_input(modifiers, key_event, events) { + // i catch these earlier + WidgetResponse::SwitchFocus(_) => return PageResponse::None, + WidgetResponse::RequestRedraw(changed) => { + if changed { + send_song_op(SongOperation::SetPan( + c, + Pan::Value(u8::try_from(slider.get()).unwrap()), + )); + } + return PageResponse::RequestRedraw; + } + WidgetResponse::None => return PageResponse::None, + } + } + Mode::Volume => { + let slider = &mut self.volume[usize::from(c - 1)]; + match slider.process_input(modifiers, key_event, events) { + // i catch these earlier + WidgetResponse::SwitchFocus(_) => return PageResponse::None, + WidgetResponse::RequestRedraw(changed) => { + if changed { + send_song_op(SongOperation::SetVolume( + c, + u8::try_from(slider.get()).unwrap(), + )); + } + return PageResponse::RequestRedraw; + } + WidgetResponse::None => return PageResponse::None, + } + } } } } PageResponse::None } + + #[cfg(feature = "accesskit")] + fn build_tree( + &self, + tree: &mut Vec<(accesskit::NodeId, accesskit::Node)>, + ) -> crate::AccessResponse { + todo!() + } } diff --git a/src/ui/pages/pattern.rs b/src/pages/pattern.rs similarity index 96% rename from src/ui/pages/pattern.rs rename to src/pages/pattern.rs index 3c883ad..4732ccd 100644 --- a/src/ui/pages/pattern.rs +++ b/src/pages/pattern.rs @@ -12,9 +12,10 @@ use winit::{ }; use crate::{ - app::{EXECUTOR, EventQueue, GlobalEvent, SONG_MANAGER, send_song_op}, + EXECUTOR, EventQueue, GlobalEvent, SONG_MANAGER, coordinates::{CharPosition, CharRect}, - ui::header::HeaderEvent, + header::HeaderEvent, + send_song_op, }; use super::{Page, PageResponse}; @@ -134,7 +135,7 @@ impl PatternPage { /// how many rows the cursor is moved when pressing pageup/down // TODO: make configurable const PAGE_AS_ROWS: u16 = 16; - const CHANNEL_WIDTH: usize = 14; + const CHANNEL_WIDTH: u8 = 14; // TODO: make configurable const ROW_HIGHTLIGHT_MINOR: u16 = 4; @@ -294,7 +295,7 @@ impl Page for PatternPage { }; draw_buffer.draw_string( from_utf8(&buf).unwrap(), - BASE_POS + CharPosition::new(0, index), + BASE_POS + CharPosition::new(0, u8::try_from(index).unwrap()), text_color, 2, ); @@ -311,7 +312,7 @@ impl Page for PatternPage { draw_buffer.draw_string( from_utf8(&buf).unwrap(), - BASE_POS + (index * Self::CHANNEL_WIDTH, 0), + BASE_POS + (u8::try_from(index).unwrap() * Self::CHANNEL_WIDTH, 0), 3, 1, ); @@ -404,7 +405,11 @@ impl Page for PatternPage { }) .map(|e| (*e).into()) .unwrap_or_default(); - let pos = EVENT_BASE_POS + (c_idx * Self::CHANNEL_WIDTH, r_idx); + let pos = EVENT_BASE_POS + + ( + u8::try_from(c_idx).unwrap() * Self::CHANNEL_WIDTH, + u8::try_from(r_idx).unwrap(), + ); draw_buffer.draw_char(view.note1, pos, 6, background_color); draw_buffer.draw_char(view.note2, pos + (1, 0), FOREGROUND, background_color); draw_buffer.draw_char(view.octave, pos + (2, 0), FOREGROUND, background_color); @@ -430,8 +435,8 @@ impl Page for PatternPage { assert!(self.cursor_position.0.channel >= self.draw_position.channel); assert!(self.cursor_position.0.row >= self.draw_position.row); let c_idx = self.cursor_position.0.channel - self.draw_position.channel; - let r_idx = self.cursor_position.0.row - self.draw_position.row; - let pos = EVENT_BASE_POS + (c_idx as usize * Self::CHANNEL_WIDTH, r_idx as usize); + let r_idx = u8::try_from(self.cursor_position.0.row - self.draw_position.row).unwrap(); + let pos = EVENT_BASE_POS + (c_idx * Self::CHANNEL_WIDTH, r_idx); match self.cursor_position.1 { InEventPosition::Note => draw_buffer.draw_char(view.note1, pos, 0, 3), InEventPosition::Octave => draw_buffer.draw_char(view.octave, pos + (2, 0), 0, 3), @@ -449,7 +454,7 @@ impl Page for PatternPage { draw_buffer.draw_rect(2, CharRect::PAGE_AREA); // draw channel headers const parts - for index in 0..Self::DRAWN_CHANNELS as usize { + for index in 0..Self::DRAWN_CHANNELS { const BASE_POS: CharPosition = CharPosition::new(5, 14); let pos = BASE_POS + (index * Self::CHANNEL_WIDTH, 0); draw_buffer.draw_rect(1, (pos + (11, 0)).into()); @@ -649,4 +654,12 @@ impl Page for PatternPage { PageResponse::None } + + #[cfg(feature = "accesskit")] + fn build_tree( + &self, + tree: &mut Vec<(accesskit::NodeId, accesskit::Node)>, + ) -> crate::AccessResponse { + todo!() + } } diff --git a/src/pages/sample_list.rs b/src/pages/sample_list.rs new file mode 100644 index 0000000..282f3d3 --- /dev/null +++ b/src/pages/sample_list.rs @@ -0,0 +1,479 @@ +use std::{ + io::{self, Write}, + iter::zip, + num::NonZero, + str::from_utf8, +}; + +use ascii::{AsciiChar, AsciiString}; +use font8x8::UnicodeFonts; +use torque_tracker_engine::{ + project::{ + note_event::Note, + song::{Song, SongOperation}, + }, + sample::{Sample, SampleMetaData}, +}; +use winit::keyboard::{Key, NamedKey}; + +use crate::{ + EXECUTOR, EventQueue, GlobalEvent, SONG_OP_SEND, + coordinates::{CharPosition, CharRect}, + draw_buffer::DrawBuffer, + header::HeaderEvent, + pages::{Page, PageEvent, PageResponse, pattern::PatternPageEvent}, + widgets::{NextWidget, WidgetResponse, text_in}, +}; + +#[derive(Debug, Clone)] +pub enum SampleListEvent { + SetSample(u8, String, SampleMetaData), + SelectSample(u8), +} + +#[derive(PartialEq, Eq, Copy, Clone)] +enum Cursor { + Name(u8), + Play, +} + +pub struct SampleList { + selected_sample: u8, + cursor: Cursor, + sample_view: u8, + samples: [Option<(AsciiString, SampleMetaData)>; Song::MAX_SAMPLES_INSTR], + event_proxy: winit::event_loop::EventLoopProxy, +} + +impl SampleList { + const SAMPLE_VIEW_COUNT: u8 = 34; + pub fn new(event_proxy: winit::event_loop::EventLoopProxy) -> Self { + Self { + selected_sample: 0, + samples: [const { None }; Song::MAX_SAMPLES_INSTR], + sample_view: 0, + event_proxy, + cursor: Cursor::Name(0), + } + } + + pub fn process_event( + &mut self, + event: SampleListEvent, + events: &mut EventQueue<'_>, + ) -> PageResponse { + match event { + // this event is from the pattern page, so i don't have to send it there + SampleListEvent::SelectSample(s) => { + self.select_sample(s); + self.send_to_header(events); + PageResponse::RequestRedraw + } + SampleListEvent::SetSample(idx, name, meta) => { + let name = name + .chars() + .flat_map(|c| AsciiChar::from_ascii(c).ok()) + .collect::(); + self.samples[usize::from(idx)] = Some((name, meta)); + if self.selected_sample == idx { + self.send_to_header(events); + } + PageResponse::RequestRedraw + } + } + } + + fn select_sample(&mut self, sample: u8) { + self.selected_sample = sample; + self.sample_view = if self.selected_sample < self.sample_view { + self.selected_sample + } else if self.selected_sample > self.sample_view + Self::SAMPLE_VIEW_COUNT { + self.selected_sample - Self::SAMPLE_VIEW_COUNT + } else { + self.sample_view + }; + } + + fn send_to_header(&self, events: &mut EventQueue<'_>) { + let name: Box = self.samples[usize::from(self.selected_sample)] + .as_ref() + .map(|(n, _)| Box::from(n.as_str())) + .unwrap_or(Box::from("")); + events.push(GlobalEvent::Header(HeaderEvent::SetSample( + self.selected_sample, + name, + ))); + } + + fn send_to_pattern(&self, events: &mut EventQueue<'_>) { + events.push(GlobalEvent::Page(PageEvent::Pattern( + PatternPageEvent::SetSampleInstr(self.selected_sample), + ))); + } + + // exists so that this montrosity doesn't sit in the middle of the keyboard input code + fn load_audio_file(&mut self) { + let dialog = rfd::AsyncFileDialog::new() + // TODO: figure out which formats i support and sync it with the symphonia features + // .add_filter("supported audio formats", &["wav"]) + .pick_file(); + let proxy = self.event_proxy.clone(); + let idx = self.selected_sample; + EXECUTOR + .spawn(async move { + let file = dialog.await; + let Some(file) = file else { + return; + }; + let file_name = file.file_name(); + // HOW TO SYMPHONIA: https://github.com/pdeljanov/Symphonia/blob/master/symphonia/examples/basic-interleaved.rs + // IO is not async as symphonia doesn't support async IO. + // This is fine as i have two background threads and don't + // do IO that often. + let Ok(file) = std::fs::File::open(file.path()) else { + eprintln!("error opening file"); + return; + }; + let mss = + symphonia::core::io::MediaSourceStream::new(Box::new(file), Default::default()); + let probe = symphonia::default::get_probe(); + let Ok(probed) = probe.format( + // TODO: add file extension to the hint + &symphonia::core::probe::Hint::new(), + mss, + &Default::default(), + &Default::default(), + ) else { + eprintln!("format error"); + return; + }; + let mut format = probed.format; + let Some(track) = format + .tracks() + .iter() + .find(|t| t.codec_params.codec != symphonia::core::codecs::CODEC_TYPE_NULL) + else { + eprintln!("no decodable track found"); + return; + }; + let Ok(mut decoder) = + symphonia::default::get_codecs().make(&track.codec_params, &Default::default()) + else { + eprintln!("no decoder found"); + return; + }; + let track_id = track.id; + let Some(sample_rate) = track.codec_params.sample_rate else { + eprintln!("no sample rate"); + return; + }; + let Some(sample_rate) = NonZero::new(sample_rate) else { + eprintln!("sample rate = 0"); + return; + }; + let mut buf = Vec::new(); + // i don't know yet. after the first iteration of the loop this is set + let mut stereo: Option = None; + loop { + let packet = format.next_packet(); + let packet = match packet { + Ok(p) => p, + // this is used as a end of stream signal. don't ask me why + Err(symphonia::core::errors::Error::IoError(e)) + if e.kind() == std::io::ErrorKind::UnexpectedEof => + { + break; + } + Err(e) => { + eprintln!("decoding error: {e:?}"); + return; + } + }; + + if packet.track_id() != track_id { + continue; + } + match decoder.decode(&packet) { + Ok(audio_buf) => { + fn append_to_buf( + buf: &mut Vec, + in_buf: &symphonia::core::audio::AudioBuffer, + stereo: &mut Option, + ) where + T: symphonia::core::sample::Sample, + f32: symphonia::core::conv::FromSample, + { + use symphonia::core::{ + audio::{Channels, Signal}, + conv::FromSample, + }; + if in_buf + .spec() + .channels + .contains(Channels::FRONT_LEFT | Channels::FRONT_RIGHT) + { + // stereo + plus maybe other channels that i ignore + assert!(stereo.is_none() || *stereo == Some(true)); + *stereo = Some(true); + let left = in_buf.chan(0); + let right = in_buf.chan(1); + assert!(left.len() == right.len()); + let iter = zip(left, right).flat_map(|(l, r)| { + [f32::from_sample(*l), f32::from_sample(*r)] + }); + buf.extend(iter); + } else if in_buf.spec().channels.contains(Channels::FRONT_LEFT) { + // assert not + assert!(stereo.is_none() || *stereo == Some(false)); + *stereo = Some(false); + buf.extend( + in_buf + .chan(0) + .iter() + .map(|sample| f32::from_sample(*sample)), + ); + } else { + eprintln!("no usable channel in sample data") + } + } + use symphonia::core::audio::AudioBufferRef; + match audio_buf { + AudioBufferRef::U8(d) => append_to_buf(&mut buf, &d, &mut stereo), + AudioBufferRef::U16(d) => append_to_buf(&mut buf, &d, &mut stereo), + AudioBufferRef::U24(d) => append_to_buf(&mut buf, &d, &mut stereo), + AudioBufferRef::U32(d) => append_to_buf(&mut buf, &d, &mut stereo), + AudioBufferRef::S8(d) => append_to_buf(&mut buf, &d, &mut stereo), + AudioBufferRef::S16(d) => append_to_buf(&mut buf, &d, &mut stereo), + AudioBufferRef::S24(d) => append_to_buf(&mut buf, &d, &mut stereo), + AudioBufferRef::S32(d) => append_to_buf(&mut buf, &d, &mut stereo), + AudioBufferRef::F32(d) => append_to_buf(&mut buf, &d, &mut stereo), + AudioBufferRef::F64(d) => append_to_buf(&mut buf, &d, &mut stereo), + } + } + Err(symphonia::core::errors::Error::DecodeError(_)) => (), + Err(_) => break, + } + } + // hopefully both of these compile to a memcopy... + let sample = if stereo.unwrap() { + Sample::new_stereo_interpolated(buf) + } else { + Sample::new_mono(buf) + }; + // TODO: get the real metadata / sane defaults / configurable + let meta = SampleMetaData { + default_volume: 32, + global_volume: 32, + default_pan: None, + vibrato_speed: 0, + vibrato_depth: 0, + vibrato_rate: 0, + vibrato_waveform: Default::default(), + sample_rate, + base_note: Note::new(64).unwrap(), + }; + // send to UI + proxy + .send_event(GlobalEvent::Page(PageEvent::SampleList( + SampleListEvent::SetSample(idx, file_name, meta), + ))) + .unwrap(); + drop(proxy); + // send to playback + let operation = SongOperation::SetSample(idx, meta, sample); + SONG_OP_SEND.get().unwrap().send(operation).await.unwrap(); + }) + .detach(); + } +} + +impl Page for SampleList { + fn draw(&mut self, draw_buffer: &mut DrawBuffer) { + // samples + play buttons + const SAMPLE_BASE_POS: CharPosition = CharPosition::new(2, 13); + const PLAY_BASE_POS: CharPosition = CharPosition::new(31, 13); + let mut buf = [0; 2]; + for (i, n) in (self.sample_view..=self.sample_view + Self::SAMPLE_VIEW_COUNT).enumerate() { + let i = u8::try_from(i).unwrap(); + // number + let mut curse: io::Cursor<&mut [u8]> = io::Cursor::new(&mut buf); + write!(curse, "{:02}", n).unwrap(); + let str = from_utf8(&buf).unwrap(); + draw_buffer.draw_string(str, SAMPLE_BASE_POS + CharPosition::new(0, i), 0, 2); + + // name + let name = self.samples[usize::from(n)].as_ref().map(|(n, _)| n); + let selected = self.selected_sample == n; + let background_color = if selected { 14 } else { 0 }; + let name_pos = SAMPLE_BASE_POS + CharPosition::new(3, i); + draw_buffer.draw_string_length( + name.unwrap_or(&AsciiString::new()).as_str(), + name_pos, + 24, + 6, + background_color, + ); + // if selected draw the text cursor by replacing one char + if selected + && let Cursor::Name(text_cursor) = self.cursor + && let Some(name) = name + { + let cursor_char_pos = name_pos + CharPosition::new(text_cursor, 0); + if usize::from(text_cursor) < name.len() { + draw_buffer.draw_char( + font8x8::BASIC_FONTS + .get(name[usize::from(text_cursor)].into()) + .unwrap(), + cursor_char_pos, + 0, + 3, + ); + } else { + draw_buffer.draw_rect(3, cursor_char_pos.into()); + } + } + + // play button + let (fg_color, bg_color) = match (selected, name.is_some(), self.cursor == Cursor::Play) + { + // row not selected + (false, false, _) => (7, 0), + (false, true, _) => (6, 0), + // row selected, sample inactive + (true, false, false) => (7, 14), + (true, false, true) => (0, 6), + // row selected, sample active + (true, true, false) => (6, 14), + (true, true, true) => (0, 3), + }; + draw_buffer.show_colors(); + draw_buffer.draw_string( + "Play", + PLAY_BASE_POS + CharPosition::new(0, i), + fg_color, + bg_color, + ); + } + } + + fn draw_constant(&mut self, draw_buffer: &mut DrawBuffer) { + draw_buffer.draw_rect(2, CharRect::PAGE_AREA); + } + + fn process_key_event( + &mut self, + modifiers: &winit::event::Modifiers, + key_event: &winit::event::KeyEvent, + events: &mut EventQueue<'_>, + ) -> PageResponse { + // TODO: remove this once buttons exist on this page + if !key_event.state.is_pressed() { + return PageResponse::None; + } + + match self.cursor { + Cursor::Name(text_cursor) => { + if key_event.logical_key == Key::Named(NamedKey::Tab) { + if modifiers.state().shift_key() { + // TODO: shift aroung to one of the buttons + } else { + self.cursor = Cursor::Play; + } + return PageResponse::RequestRedraw; + } + if let Some(sample) = &mut self.samples[usize::from(self.selected_sample)] { + let mut text_cursor = usize::from(text_cursor); + // text_editing + let resp = text_in::process_input( + &mut sample.0, + 24, + &NextWidget::default(), + &mut text_cursor, + None, + modifiers, + key_event, + ); + let text_cursor = u8::try_from(text_cursor).expect( + "process input has increased the cursor outside of the text bounds", + ); + match resp { + // no next widget specified + WidgetResponse::SwitchFocus(_) => unreachable!(), + // need to update the header + WidgetResponse::RequestRedraw(true) => { + self.send_to_header(events); + self.cursor = Cursor::Name(text_cursor); + // data changed, so early return redraw + return PageResponse::RequestRedraw; + } + WidgetResponse::RequestRedraw(false) => { + // cursor movement, so early return + self.cursor = Cursor::Name(text_cursor); + // here the header doesn't have to be updated, because only the + // cursor position changed + return PageResponse::RequestRedraw; + } + WidgetResponse::None => (), + } + } + } + Cursor::Play => { + if key_event.logical_key == Key::Named(NamedKey::Tab) { + if modifiers.state().shift_key() { + // set the text_cursor to the end, because i came from the right + let name_len = self.samples[usize::from(self.selected_sample)] + .as_ref() + .map(|(s, _)| s.len()) + .unwrap_or(0); + self.cursor = Cursor::Name(name_len.try_into().unwrap()); + return PageResponse::RequestRedraw; + } else { + // TODO: move to one of the sample controls + return PageResponse::None; + } + } + // trigger a oneshot playback of the selected sample + } + } + + // if this matches the cursor is in the sample list + if matches!(self.cursor, Cursor::Play | Cursor::Name(_)) { + if key_event.logical_key == Key::Named(NamedKey::ArrowUp) + && modifiers.state().is_empty() + { + if let Some(s) = self.selected_sample.checked_sub(1) { + self.select_sample(s); + self.send_to_header(events); + self.send_to_pattern(events); + return PageResponse::RequestRedraw; + } + } else if key_event.logical_key == Key::Named(NamedKey::ArrowDown) + && modifiers.state().is_empty() + { + if self.selected_sample + 1 < 100 { + self.select_sample(self.selected_sample + 1); + self.send_to_header(events); + self.send_to_pattern(events); + return PageResponse::RequestRedraw; + } + } else if key_event.logical_key == Key::Named(NamedKey::Enter) + && modifiers.state().is_empty() + { + self.load_audio_file(); + } + // TODO: add PageUp and PageDown + } else { + todo!("other UI elements that are per sample") + } + + PageResponse::None + } + + #[cfg(feature = "accesskit")] + fn build_tree( + &self, + tree: &mut Vec<(accesskit::NodeId, accesskit::Node)>, + ) -> crate::AccessResponse { + todo!() + } +} diff --git a/src/ui/pages/song_directory_config_page.rs b/src/pages/song_directory_config_page.rs similarity index 62% rename from src/ui/pages/song_directory_config_page.rs rename to src/pages/song_directory_config_page.rs index 4641137..be21542 100644 --- a/src/ui/pages/song_directory_config_page.rs +++ b/src/pages/song_directory_config_page.rs @@ -3,10 +3,15 @@ use std::num::NonZero; use torque_tracker_engine::project::song::SongOperation; use crate::{ - app::{EventQueue, GlobalEvent, send_song_op}, + EventQueue, GlobalEvent, coordinates::{CharPosition, CharRect}, draw_buffer::DrawBuffer, - ui::widgets::{NextWidget, StandardResponse, WidgetResponse, slider::Slider, text_in::TextIn}, + send_song_op, + widgets::{ + NextWidget, Widget, WidgetResponse, + slider::Slider, + text_in::{TextIn, TextInScroll}, + }, }; use super::{Page, PageResponse}; @@ -39,43 +44,59 @@ pub enum SDCChange { // Seperation(i16), } -super::create_widget_list!( - response: (); - WidgetList - { - song_name: TextIn<()>, - initial_tempo: Slider<31, 255, ()>, - initial_speed: Slider<1, 255, ()>, - global_volume: Slider<0, 128, ()> - // mixing_volume: Slider<0, 128, ()>, - // seperation: Slider<0, 128, ()>, - - // old_effects: Toggle, - // compatible_gxx: Toggle, - - // instruments: ToggleButton, - // samples: ToggleButton, - - // stereo: ToggleButton, - // mono: ToggleButton, - - // linear_slides: ToggleButton, - // amiga_slides: ToggleButton, - - // module_path: TextInScroll<()>, - // sample_path: TextInScroll<()>, - // instrument_path: TextInScroll<()>, - // save: Button<()> - } -); +// super::create_widget_list!( +// response: (); +// WidgetList +// { +// song_name: TextIn<()>, +// initial_tempo: Slider<31, 255, ()>, +// initial_speed: Slider<1, 255, ()>, +// global_volume: Slider<0, 128, ()> +// // mixing_volume: Slider<0, 128, ()>, +// // seperation: Slider<0, 128, ()>, + +// // old_effects: Toggle, +// // compatible_gxx: Toggle, + +// // instruments: ToggleButton, +// // samples: ToggleButton, + +// // stereo: ToggleButton, +// // mono: ToggleButton, + +// // linear_slides: ToggleButton, +// // amiga_slides: ToggleButton, + +// // module_path: TextInScroll<()>, +// // sample_path: TextInScroll<()>, +// // instrument_path: TextInScroll<()>, +// // save: Button<()> +// } +// ); pub struct SongDirectoryConfigPage { - widgets: WidgetList, + // widgets: WidgetList, + song_name: TextIn, + initial_tempo: Slider<31, 255>, + initial_speed: Slider<1, 255>, + global_volume: Slider<0, 128>, + instrument_path: TextInScroll, + selected: u8, } impl Page for SongDirectoryConfigPage { fn draw(&mut self, draw_buffer: &mut DrawBuffer) { - self.widgets.draw_widgets(draw_buffer); + // self.widgets.draw_widgets(draw_buffer); + self.song_name + .draw(draw_buffer, Self::SONG_NAME == self.selected); + self.initial_tempo + .draw(draw_buffer, Self::INITIAL_TEMPO == self.selected); + self.initial_speed + .draw(draw_buffer, Self::INITIAL_SPEED == self.selected); + self.global_volume + .draw(draw_buffer, Self::GLOBAL_VOLUME == self.selected); + self.instrument_path + .draw(draw_buffer, Self::INSTRUMENT_PATH == self.selected); } fn draw_constant(&mut self, draw_buffer: &mut DrawBuffer) { @@ -147,37 +168,107 @@ impl Page for SongDirectoryConfigPage { key_event: &winit::event::KeyEvent, events: &mut EventQueue<'_>, ) -> PageResponse { - match self - .widgets - .process_input(key_event, modifiers, events) - .standard - { - StandardResponse::SwitchFocus(next) => { - self.widgets.selected = next; - PageResponse::RequestRedraw - } - StandardResponse::RequestRedraw => PageResponse::RequestRedraw, - StandardResponse::None => PageResponse::None, + let resp = match self.selected { + Self::SONG_NAME => self + .song_name + .process_input(modifiers, key_event, events) + .on_change(|| self.song_name_changed()), + Self::INITIAL_TEMPO => self + .initial_tempo + .process_input(modifiers, key_event, events) + .on_change(|| self.initial_tempo_changed()), + Self::INITIAL_SPEED => self + .initial_speed + .process_input(modifiers, key_event, events) + .on_change(|| self.initial_tempo_changed()), + Self::GLOBAL_VOLUME => self + .global_volume + .process_input(modifiers, key_event, events) + .on_change(|| self.global_volume_changed()), + Self::INSTRUMENT_PATH => self + .instrument_path + .process_input(modifiers, key_event, events), + _ => unreachable!(), + }; + + resp.to_page_resp(&mut self.selected) + } + + #[cfg(feature = "accesskit")] + fn build_tree( + &self, + tree: &mut Vec<(accesskit::NodeId, accesskit::Node)>, + ) -> crate::AccessResponse { + use accesskit::{Node, NodeId, Role}; + + use crate::AccessResponse; + + let mut root_node = Node::new(Role::Menu); + let nodes = [ + NodeId(Self::SONG_NAME_ID), + NodeId(Self::INITIAL_TEMPO_ID), + NodeId(Self::INITIAL_SPEED_ID), + NodeId(Self::GLOBAL_VOLUME_ID), + ]; + root_node.set_children(nodes); + root_node.set_label("Song Directory Config Page"); + + self.song_name.build_tree(tree); + self.initial_tempo.build_tree(tree); + self.initial_speed.build_tree(tree); + self.global_volume.build_tree(tree); + + tree.push((NodeId(Self::PAGE_ID), root_node)); + AccessResponse { + root: NodeId(Self::PAGE_ID), + selected: nodes[usize::from(self.selected - 1)], } } } impl SongDirectoryConfigPage { + const PAGE_ID: u64 = 12_000_000_000; + const SONG_NAME: u8 = 1; + const SONG_NAME_ID: u64 = Self::PAGE_ID + Self::SONG_NAME as u64 * 20; + const INITIAL_TEMPO: u8 = 2; + const INITIAL_TEMPO_ID: u64 = Self::PAGE_ID + Self::INITIAL_TEMPO as u64 * 20; + const INITIAL_SPEED: u8 = 3; + const INITIAL_SPEED_ID: u64 = Self::PAGE_ID + Self::INITIAL_SPEED as u64 * 20; + const GLOBAL_VOLUME: u8 = 4; + const GLOBAL_VOLUME_ID: u64 = Self::PAGE_ID + Self::GLOBAL_VOLUME as u64 * 20; + const INSTRUMENT_PATH: u8 = 5; + pub fn ui_change(&mut self, change: SDCChange) -> PageResponse { match change { - SDCChange::SetSongName(s) => match self.widgets.song_name.set_string(s) { + SDCChange::SetSongName(s) => match self + .song_name + .set_string(s) + .map(|_| self.song_name_changed()) + { Ok(_) => PageResponse::RequestRedraw, Err(_) => PageResponse::None, }, - SDCChange::InitialTempo(n) => match self.widgets.initial_tempo.try_set(n) { + SDCChange::InitialTempo(n) => match self + .initial_tempo + .try_set(n) + .map(|_| self.initial_tempo_changed()) + { Ok(_) => PageResponse::RequestRedraw, Err(_) => PageResponse::None, }, - SDCChange::InitialSpeed(n) => match self.widgets.initial_speed.try_set(n) { + SDCChange::InitialSpeed(n) => match self + .initial_speed + .try_set(n) + .map(|_| self.initial_speed_changed()) + { Ok(_) => PageResponse::RequestRedraw, Err(_) => PageResponse::None, }, - SDCChange::GlobalVolume(n) => match self.widgets.global_volume.try_set(n) { + SDCChange::GlobalVolume(n) => match self + .global_volume + .try_set(n) + .map(|_| self.global_volume_changed()) + { Ok(_) => PageResponse::RequestRedraw, Err(_) => PageResponse::None, }, @@ -192,66 +283,97 @@ impl SongDirectoryConfigPage { } } + fn song_name_changed(&self) { + println!("song name: {}", self.song_name.get_str()) + } + + fn initial_tempo_changed(&self) { + send_song_op(SongOperation::SetInitialTempo( + NonZero::new(u8::try_from(self.initial_tempo.get()).unwrap()).unwrap(), + )); + } + + fn initial_speed_changed(&self) { + send_song_op(SongOperation::SetInitialSpeed( + NonZero::new(u8::try_from(self.initial_speed.get()).unwrap()).unwrap(), + )); + } + + fn global_volume_changed(&self) { + send_song_op(SongOperation::SetGlobalVol( + u8::try_from(self.global_volume.get()).unwrap(), + )); + } + pub fn new() -> Self { let song_name = TextIn::new( CharPosition::new(17, 16), 25, NextWidget { - down: Some(WidgetList::INITIAL_TEMPO), - tab: Some(WidgetList::INITIAL_TEMPO), + down: Some(Self::INITIAL_TEMPO), + tab: Some(Self::INITIAL_TEMPO), ..Default::default() }, - |s| println!("new song name: {}", s), + #[cfg(feature = "accesskit")] + ( + accesskit::NodeId(Self::PAGE_ID + u64::from(Self::SONG_NAME) * 20), + "Song Name", + ), ); let initial_tempo = Slider::new( 125, CharPosition::new(17, 19), 32, NextWidget { - up: Some(WidgetList::SONG_NAME), - shift_tab: Some(WidgetList::SONG_NAME), - down: Some(WidgetList::INITIAL_SPEED), - tab: Some(WidgetList::INITIAL_SPEED), + up: Some(Self::SONG_NAME), + shift_tab: Some(Self::SONG_NAME), + down: Some(Self::INITIAL_SPEED), + tab: Some(Self::INITIAL_SPEED), ..Default::default() }, |n| GlobalEvent::Page(super::PageEvent::Sdc(SDCChange::InitialTempo(n))), - |value| { - send_song_op(SongOperation::SetInitialTempo( - NonZero::new(u8::try_from(value).unwrap()).unwrap(), - )); - }, + #[cfg(feature = "accesskit")] + ( + accesskit::NodeId(Self::PAGE_ID + u64::from(Self::INITIAL_TEMPO) * 20), + "Initial Tempo".into(), + ), ); let initial_speed = Slider::new( 6, CharPosition::new(17, 20), 32, NextWidget { - up: Some(WidgetList::INITIAL_TEMPO), - shift_tab: Some(WidgetList::INITIAL_TEMPO), - down: Some(WidgetList::GLOBAL_VOLUME), - tab: Some(WidgetList::GLOBAL_VOLUME), + up: Some(Self::INITIAL_TEMPO), + shift_tab: Some(Self::INITIAL_TEMPO), + down: Some(Self::GLOBAL_VOLUME), + tab: Some(Self::GLOBAL_VOLUME), ..Default::default() }, |n| GlobalEvent::Page(super::PageEvent::Sdc(SDCChange::InitialSpeed(n))), - |value| { - send_song_op(SongOperation::SetInitialSpeed( - NonZero::new(u8::try_from(value).unwrap()).unwrap(), - )); - }, + #[cfg(feature = "accesskit")] + ( + accesskit::NodeId(Self::PAGE_ID + u64::from(Self::INITIAL_SPEED) * 20), + "Initial Speed".into(), + ), ); let global_volume = Slider::new( 128, CharPosition::new(17, 23), 16, NextWidget { - up: Some(WidgetList::INITIAL_SPEED), - shift_tab: Some(WidgetList::INITIAL_SPEED), - // down: Some(WidgetList::MIXING_VOLUME), - // tab: Some(WidgetList::MIXING_VOLUME), + up: Some(Self::INITIAL_SPEED), + shift_tab: Some(Self::INITIAL_SPEED), + // down: Some(Self::MIXING_VOLUME), + down: Some(Self::INSTRUMENT_PATH), + // tab: Some(Self::MIXING_VOLUME), ..Default::default() }, |n| GlobalEvent::Page(super::PageEvent::Sdc(SDCChange::GlobalVolume(n))), - |value| send_song_op(SongOperation::SetGlobalVol(u8::try_from(value).unwrap())), + #[cfg(feature = "accesskit")] + ( + accesskit::NodeId(Self::PAGE_ID + u64::from(Self::GLOBAL_VOLUME) * 20), + "Global Volume".into(), + ), ); // let mixing_volume = Slider::new( // 48, @@ -433,18 +555,17 @@ impl SongDirectoryConfigPage { // }, // |text| println!("Sample path set to {text}"), // ); - // let instrument_path = TextInScroll::new( - // CharPosition::new(13, 44), - // 64, - // NextWidget { - // up: Some(WidgetList::MODULE_PATH), - // shift_tab: Some(WidgetList::MODULE_PATH), - // down: Some(WidgetList::SAVE), - // tab: Some(WidgetList::SAVE), - // ..Default::default() - // }, - // |text| println!("Instrument path set to {text}"), - // ); + let instrument_path = TextInScroll::new( + CharPosition::new(13, 44), + 64, + NextWidget { + up: Some(Self::GLOBAL_VOLUME), + // shift_tab: Some(Self::MODULE_PATH), + // down: Some(Self::SAVE), + // tab: Some(Self::SAVE), + ..Default::default() + }, + ); // let save = Button::new( // "Save all Preferences", @@ -458,27 +579,25 @@ impl SongDirectoryConfigPage { // }, // ); Self { - widgets: WidgetList { - song_name, - initial_tempo, - initial_speed, - global_volume, - // mixing_volume, - // seperation, - // old_effects, - // compatible_gxx, - // instruments, - // samples, - // stereo, - // mono, - // linear_slides, - // amiga_slides, - // module_path, - // sample_path, - // instrument_path, - // save, - selected: WidgetList::SONG_NAME, - }, + song_name, + initial_tempo, + initial_speed, + global_volume, + // mixing_volume, + // seperation, + // old_effects, + // compatible_gxx, + // instruments, + // samples, + // stereo, + // mono, + // linear_slides, + // amiga_slides, + // module_path, + // sample_path, + instrument_path, + // save, + selected: Self::SONG_NAME, } } } diff --git a/src/gpu.rs b/src/render/gpu.rs similarity index 91% rename from src/gpu.rs rename to src/render/gpu.rs index f18f51c..b8c7af8 100644 --- a/src/gpu.rs +++ b/src/render/gpu.rs @@ -13,9 +13,11 @@ use wgpu::{ }; use winit::window::Window; -use crate::palettes::{PALETTE_SIZE, Palette, RGB10A2}; +use super::palettes::{PALETTE_SIZE, Palette, RGB10A2}; -use super::coordinates::WINDOW_SIZE; +use crate::coordinates::WINDOW_SIZE; +// use crate::coordinates::WINDOW_SIZE as WINDOW_SIZE16; +// const WINDOW_SIZE: (usize, usize) = (WINDOW_SIZE16.0 as usize, WINDOW_SIZE16.1 as usize); pub struct GPUState { surface: Surface<'static>, @@ -46,11 +48,13 @@ impl GPUState { depth_or_array_layers: 1, }; - pub async fn new(window: std::sync::Arc) -> Self { + // this should never panic to allow using the software scaling if it's enabled + pub async fn new(window: std::sync::Arc) -> Result { let size = window.inner_size(); let instance = Instance::default(); - let surface: Surface<'static> = instance.create_surface(window).unwrap(); + let surface: Surface<'static> = + instance.create_surface(window).map_err(|e| e.to_string())?; let adapter = instance .request_adapter(&RequestAdapterOptions { @@ -61,7 +65,7 @@ impl GPUState { force_fallback_adapter: false, }) .await - .unwrap(); + .map_err(|e| e.to_string())?; let (device, queue) = adapter .request_device(&DeviceDescriptor { @@ -72,21 +76,25 @@ impl GPUState { // so i don't need a lot of allocations and they don't have to be that fast memory_hints: wgpu::MemoryHints::MemoryUsage, trace: wgpu::Trace::Off, + experimental_features: wgpu::ExperimentalFeatures::disabled(), }) .await - .unwrap(); + .map_err(|e| e.to_string())?; let surface_caps = surface.get_capabilities(&adapter); - let surface_format = *surface_caps - .formats - .iter() - .find(|f| !f.is_srgb()) - .unwrap_or(&surface_caps.formats[0]); + let surface_format = { + let surface_opt = surface_caps.formats.iter().find(|f| !f.is_srgb()); + match (surface_opt, surface_caps.formats.first()) { + (Some(f), _) => f, + (None, Some(f)) => f, + (None, None) => return Err(String::from("No surface capabilites available")), + } + }; let config = SurfaceConfiguration { usage: TextureUsages::RENDER_ATTACHMENT, - format: surface_format, + format: *surface_format, width: size.width, height: size.height, present_mode: surface_caps.present_modes[0], @@ -267,7 +275,7 @@ impl GPUState { cache: None, }); - Self { + Ok(Self { surface, device, queue, @@ -277,7 +285,7 @@ impl GPUState { diffuse_bind_group, streaming_texture: window_texture, color_map, - } + }) } /// on next render the new palette will be used @@ -319,12 +327,11 @@ impl GPUState { pub fn render( &mut self, - framebuffer: &[[u8; WINDOW_SIZE.0]; WINDOW_SIZE.1], + framebuffer: &[[u8; WINDOW_SIZE.0 as usize]; WINDOW_SIZE.1 as usize], ) -> Result<(), SurfaceError> { // SAFETY: let framebuffer = framebuffer.as_flattened(); - assert!(framebuffer.len() == WINDOW_SIZE.0 * WINDOW_SIZE.1); const BYTES_PER_ROW: Option = Some(WINDOW_SIZE.0 as u32); const ROWS_PER_IMAGE: Option = Some(WINDOW_SIZE.1 as u32); @@ -391,4 +398,8 @@ impl GPUState { Ok(()) } + + pub fn size(&self) -> winit::dpi::PhysicalSize { + self.size + } } diff --git a/src/render.rs b/src/render/mod.rs similarity index 53% rename from src/render.rs rename to src/render/mod.rs index cdddb85..4e54642 100644 --- a/src/render.rs +++ b/src/render/mod.rs @@ -1,30 +1,84 @@ +#[cfg(feature = "gpu_scaling")] +pub mod gpu; +pub mod palettes; + use std::sync::Arc; use winit::{dpi::PhysicalSize, event_loop::ActiveEventLoop, window::Window}; -use crate::{ - coordinates::WINDOW_SIZE, - palettes::{Palette, RGB8}, -}; - -#[cfg(all(feature = "gpu_scaling", feature = "soft_scaling"))] -compile_error!("it's impossible to have both gpu and software scaling enabled"); +use crate::coordinates::WINDOW_SIZE as WINDOW_SIZE16; +const WINDOW_SIZE: (usize, usize) = (WINDOW_SIZE16.0 as usize, WINDOW_SIZE16.1 as usize); +use palettes::{Palette, RGB8}; #[cfg(not(any(feature = "gpu_scaling", feature = "soft_scaling")))] compile_error!("at least one of gpu_scaling or soft_scaling needs to be active"); +#[expect( + clippy::large_enum_variant, + reason = "this doesn't get moved a lot, so it doesn't matter. If it's not in the enum i also don't box it" +)] +#[cfg(all(feature = "gpu_scaling", feature = "soft_scaling"))] +pub enum BothRenderBackend { + GPU(GPURenderBackend), + Soft(SoftRenderBackend), +} + +#[cfg(all(feature = "gpu_scaling", feature = "soft_scaling"))] +impl BothRenderBackend { + pub fn new(window: Arc, palette: Palette) -> Self { + match GPURenderBackend::try_new(window.clone(), palette) { + Ok(b) => Self::GPU(b), + Err(e) => { + eprintln!( + "GPU render backend creation failed: {e}.\n\nFalling back to software scaling" + ); + Self::Soft(SoftRenderBackend::new(window, palette)) + } + } + } + + pub fn resize(&mut self, size: PhysicalSize) { + match self { + BothRenderBackend::GPU(b) => b.resize(size), + BothRenderBackend::Soft(b) => b.resize(size), + } + } + + pub fn render( + &mut self, + frame_buffer: &[[u8; WINDOW_SIZE.0]; WINDOW_SIZE.1], + event_loop: &ActiveEventLoop, + ) { + match self { + BothRenderBackend::GPU(b) => b.render(frame_buffer, event_loop), + BothRenderBackend::Soft(b) => b.render(frame_buffer, event_loop), + } + } + + pub fn get_size(&self) -> PhysicalSize { + match self { + BothRenderBackend::GPU(b) => b.get_size(), + BothRenderBackend::Soft(b) => b.get_size(), + } + } +} + #[cfg(feature = "gpu_scaling")] -pub struct RenderBackend { - backend: crate::gpu::GPUState, +pub struct GPURenderBackend { + backend: gpu::GPUState, } #[cfg(feature = "gpu_scaling")] -impl RenderBackend { - pub fn new(window: Arc, palette: Palette) -> Self { - let mut backend = smol::block_on(crate::gpu::GPUState::new(window)); +impl GPURenderBackend { + fn try_new(window: Arc, palette: Palette) -> Result { + let mut backend = smol::block_on(gpu::GPUState::new(window))?; backend.queue_palette_update(palette.into()); - Self { backend } + Ok(Self { backend }) + } + + pub fn new(window: Arc, palette: Palette) -> Self { + Self::try_new(window, palette).unwrap() } pub fn resize(&mut self, size: PhysicalSize) { @@ -43,18 +97,23 @@ impl RenderBackend { Err(e) => eprint!("{:?}", e), } } + + pub fn get_size(&self) -> PhysicalSize { + self.backend.size() + } } +// softscaling is small enough to not have its own file / module #[cfg(feature = "soft_scaling")] -pub struct RenderBackend { +pub struct SoftRenderBackend { backend: softbuffer::Surface, Arc>, width: u32, height: u32, - palette: Palette, + palette: Palette, } #[cfg(feature = "soft_scaling")] -impl RenderBackend { +impl SoftRenderBackend { pub fn new(window: Arc, palette: Palette) -> Self { let size = window.inner_size(); let context = softbuffer::Context::new(window.clone()).unwrap(); @@ -100,4 +159,11 @@ impl RenderBackend { } buffer.present().unwrap(); } + + pub fn get_size(&self) -> PhysicalSize { + PhysicalSize { + width: self.width, + height: self.height, + } + } } diff --git a/src/palettes.rs b/src/render/palettes.rs similarity index 99% rename from src/palettes.rs rename to src/render/palettes.rs index 3cd874d..c7c8d12 100644 --- a/src/palettes.rs +++ b/src/render/palettes.rs @@ -57,6 +57,7 @@ impl From for RGBA8 { } #[repr(transparent)] +#[derive(Clone, Copy)] pub struct Palette(pub [Color; PALETTE_SIZE]); // only needed, because its easier to set u8 color values than u10 by hand, so i store them in u8 and convert to RGB10A2 diff --git a/src/shader.wgsl b/src/render/shader.wgsl similarity index 100% rename from src/shader.wgsl rename to src/render/shader.wgsl diff --git a/src/ui/.DS_Store b/src/ui/.DS_Store deleted file mode 100644 index 134c854..0000000 Binary files a/src/ui/.DS_Store and /dev/null differ diff --git a/src/ui/dialog.rs b/src/ui/dialog.rs deleted file mode 100644 index e8a2148..0000000 --- a/src/ui/dialog.rs +++ /dev/null @@ -1,70 +0,0 @@ -pub mod confirm; -pub mod page_menu; -pub mod slider_dialog; - -use winit::event::{KeyEvent, Modifiers}; - -use crate::{app::EventQueue, draw_buffer::DrawBuffer}; - -pub enum DialogResponse { - RequestRedraw, - // should also close all Dialogs - // SwitchToPage(PagesEnum), - Close, - /// (global_event to be sent, should close the current dialog) - // GlobalEvent(GlobalEvent, bool), - None, -} - -pub trait Dialog { - fn draw(&self, draw_buffer: &mut DrawBuffer); - fn process_input( - &mut self, - key_event: &KeyEvent, - modifiers: &Modifiers, - events: &mut EventQueue<'_>, - ) -> DialogResponse; -} - -pub struct DialogManager { - stack: Vec>, -} - -impl DialogManager { - pub fn new() -> Self { - // try to match the capacity to the actually used maximum depth - Self { - stack: Vec::with_capacity(3), - } - } - - pub fn active_dialog_mut(&mut self) -> Option<&mut dyn Dialog> { - match self.stack.last_mut() { - Some(dialog) => Some(dialog.as_mut()), - None => None, - } - } - - pub fn is_active(&self) -> bool { - !self.stack.is_empty() - } - - pub fn open_dialog(&mut self, dialog: Box) { - self.stack.push(dialog); - } - - pub fn close_dialog(&mut self) { - self.stack.pop(); - } - - pub fn close_all(&mut self) { - self.stack.clear(); - } - - /// draws all currently open dialogs - pub fn draw(&self, draw_buffer: &mut DrawBuffer) { - self.stack - .iter() - .for_each(|dialog| dialog.draw(draw_buffer)); - } -} diff --git a/src/ui/dialog/confirm.rs b/src/ui/dialog/confirm.rs deleted file mode 100644 index 2b06edb..0000000 --- a/src/ui/dialog/confirm.rs +++ /dev/null @@ -1,114 +0,0 @@ -use winit::keyboard::{Key, NamedKey}; - -use crate::{ - app::GlobalEvent, - coordinates::{CharPosition, CharRect}, - draw_buffer::DrawBuffer, - ui::{ - pages::create_widget_list, - widgets::{NextWidget, StandardResponse, WidgetResponse, button::Button}, - }, -}; - -use super::{Dialog, DialogResponse}; - -create_widget_list!( - response: Option; - WidgetList - { - ok: Button>, - cancel: Button> - } -); - -pub struct ConfirmDialog { - text: &'static str, - text_pos: CharPosition, - // computed from the string length - rect: CharRect, - widgets: WidgetList, -} - -impl ConfirmDialog { - const OK_RECT: CharRect = CharRect::new(29, 31, 41, 50); - const CANCEL_RECT: CharRect = CharRect::new(29, 31, 30, 39); - pub fn new( - text: &'static str, - ok_event: fn() -> Option, - cancel_event: fn() -> Option, - ) -> Self { - let width = (text.len() + 10).max(22); - let per_side = width / 2; - Self { - text, - text_pos: CharPosition::new(40 - per_side + 5, 27), - widgets: WidgetList { - selected: WidgetList::OK, - ok: Button::new( - " Ok", - Self::OK_RECT, - NextWidget { - left: Some(WidgetList::CANCEL), - right: Some(WidgetList::CANCEL), - tab: Some(WidgetList::CANCEL), - shift_tab: Some(WidgetList::CANCEL), - ..Default::default() - }, - ok_event, - ), - cancel: Button::new( - "Cancel", - Self::CANCEL_RECT, - NextWidget { - left: Some(WidgetList::OK), - right: Some(WidgetList::OK), - tab: Some(WidgetList::OK), - shift_tab: Some(WidgetList::OK), - ..Default::default() - }, - cancel_event, - ), - }, - rect: CharRect::new(25, 32, 40 - per_side, 40 + per_side), - } - } -} - -impl Dialog for ConfirmDialog { - fn draw(&self, draw_buffer: &mut DrawBuffer) { - draw_buffer.draw_rect(2, self.rect); - draw_buffer.draw_out_border(self.rect, 3, 3, 2); - draw_buffer.draw_string(self.text, self.text_pos, 0, 2); - self.widgets.draw_widgets(draw_buffer); - } - - fn process_input( - &mut self, - key_event: &winit::event::KeyEvent, - modifiers: &winit::event::Modifiers, - events: &mut crate::app::EventQueue<'_>, - ) -> DialogResponse { - if key_event.logical_key == Key::Named(NamedKey::Escape) && modifiers.state().is_empty() { - return DialogResponse::Close; - } - - let WidgetResponse { standard, extra } = - self.widgets.process_input(key_event, modifiers, events); - if let Some(global_option) = extra { - if let Some(global) = global_option { - events.push(global); - } - // if there is a response i also want to close myself - return DialogResponse::Close; - } - - match standard { - StandardResponse::SwitchFocus(next) => { - self.widgets.selected = next; - DialogResponse::RequestRedraw - } - StandardResponse::RequestRedraw => DialogResponse::RequestRedraw, - StandardResponse::None => DialogResponse::None, - } - } -} diff --git a/src/ui/mod.rs b/src/ui/mod.rs deleted file mode 100644 index ae379c3..0000000 --- a/src/ui/mod.rs +++ /dev/null @@ -1,4 +0,0 @@ -pub mod dialog; -pub mod header; -pub mod pages; -pub mod widgets; diff --git a/src/ui/pages/sample_list.rs b/src/ui/pages/sample_list.rs deleted file mode 100644 index a9102e8..0000000 --- a/src/ui/pages/sample_list.rs +++ /dev/null @@ -1,366 +0,0 @@ -use std::{ - io::{Cursor, Write}, - iter::zip, - num::NonZero, - str::from_utf8, -}; - -use torque_tracker_engine::{ - project::{ - note_event::Note, - song::{Song, SongOperation}, - }, - sample::{Sample, SampleMetaData}, -}; -use winit::keyboard::{Key, NamedKey}; - -use crate::{ - app::{EXECUTOR, EventQueue, GlobalEvent, SONG_OP_SEND}, - coordinates::{CharPosition, CharRect}, - draw_buffer::DrawBuffer, - ui::{ - header::HeaderEvent, - pages::{Page, PageEvent, PageResponse, pattern::PatternPageEvent}, - }, -}; - -#[derive(Debug, Clone)] -pub enum SampleListEvent { - SetSample(u8, String, SampleMetaData), - SelectSample(u8), -} - -pub struct SampleList { - selected: u8, - sample_view: u8, - samples: [Option<(String, SampleMetaData)>; Song::MAX_SAMPLES_INSTR], - event_proxy: winit::event_loop::EventLoopProxy, -} - -impl SampleList { - const SAMPLE_VIEW_COUNT: u8 = 34; - pub fn new(event_proxy: winit::event_loop::EventLoopProxy) -> Self { - Self { - selected: 0, - samples: [const { None }; Song::MAX_SAMPLES_INSTR], - sample_view: 0, - event_proxy, - } - } - - pub fn process_event( - &mut self, - event: SampleListEvent, - events: &mut EventQueue<'_>, - ) -> PageResponse { - match event { - // this event is from the pattern page, so i don't have to send it there - SampleListEvent::SelectSample(s) => { - self.select_sample(s); - self.send_to_header(events); - PageResponse::RequestRedraw - } - SampleListEvent::SetSample(idx, name, meta) => { - self.samples[usize::from(idx)] = Some((name, meta)); - if self.selected == idx { - self.send_to_header(events); - } - PageResponse::RequestRedraw - } - } - } - - fn select_sample(&mut self, selected: u8) { - self.selected = selected; - self.sample_view = if self.selected < self.sample_view { - self.selected - } else if self.selected > self.sample_view + Self::SAMPLE_VIEW_COUNT { - self.selected - Self::SAMPLE_VIEW_COUNT - } else { - self.sample_view - }; - } - - fn send_to_header(&self, events: &mut EventQueue<'_>) { - let name: Box = self.samples[usize::from(self.selected)] - .as_ref() - .map(|(n, _)| Box::from(n.as_str())) - .unwrap_or(Box::from("")); - events.push(GlobalEvent::Header(HeaderEvent::SetSample( - self.selected, - name, - ))); - } - - fn send_to_pattern(&self, events: &mut EventQueue<'_>) { - events.push(GlobalEvent::Page(PageEvent::Pattern( - PatternPageEvent::SetSampleInstr(self.selected), - ))); - } -} - -impl Page for SampleList { - fn draw(&mut self, draw_buffer: &mut DrawBuffer) { - // samples - { - const BASE_POS: CharPosition = CharPosition::new(2, 13); - let mut buf = [0; 2]; - for (i, n) in - (self.sample_view..=self.sample_view + Self::SAMPLE_VIEW_COUNT).enumerate() - { - // number - let mut curse: Cursor<&mut [u8]> = Cursor::new(&mut buf); - write!(curse, "{:02}", n).unwrap(); - let str = from_utf8(&buf).unwrap(); - draw_buffer.draw_string(str, BASE_POS + CharPosition::new(0, i), 0, 2); - - // name - let name = self.samples[usize::from(n)] - .as_ref() - .map(|(n, _)| n.as_str()) - .unwrap_or(""); - let background_color = if self.selected == n { 14 } else { 0 }; - draw_buffer.draw_string_length( - name, - BASE_POS + CharPosition::new(3, i), - 24, - 6, - background_color, - ); - } - } - } - - fn draw_constant(&mut self, draw_buffer: &mut DrawBuffer) { - draw_buffer.draw_rect(2, CharRect::PAGE_AREA); - } - - fn process_key_event( - &mut self, - modifiers: &winit::event::Modifiers, - key_event: &winit::event::KeyEvent, - events: &mut EventQueue<'_>, - ) -> PageResponse { - if !key_event.state.is_pressed() { - return PageResponse::None; - } - - if key_event.logical_key == Key::Named(NamedKey::ArrowUp) && modifiers.state().is_empty() { - if let Some(s) = self.selected.checked_sub(1) { - self.select_sample(s); - self.send_to_header(events); - self.send_to_pattern(events); - return PageResponse::RequestRedraw; - } - } else if key_event.logical_key == Key::Named(NamedKey::ArrowDown) - && modifiers.state().is_empty() - { - if self.selected + 1 < 100 { - self.select_sample(self.selected + 1); - self.send_to_header(events); - self.send_to_pattern(events); - return PageResponse::RequestRedraw; - } - } else if key_event.logical_key == Key::Named(NamedKey::Enter) - && modifiers.state().is_empty() - { - let dialog = rfd::AsyncFileDialog::new() - // TODO: figure out which formats i support and sync it with the symphonia features - // .add_filter("supported audio formats", &["wav"]) - .pick_file(); - let proxy = self.event_proxy.clone(); - let idx = self.selected; - EXECUTOR - .spawn(async move { - let file = dialog.await; - let Some(file) = file else { - return; - }; - let file_name = file.file_name(); - // HOW TO SYMPHONIA: https://github.com/pdeljanov/Symphonia/blob/master/symphonia/examples/basic-interleaved.rs - // IO is not async as symphonia doesn't support async IO. - // This is fine as i have two background threads and don't - // do IO that often. - let Ok(file) = std::fs::File::open(file.path()) else { - eprintln!("error opening file"); - return; - }; - let mss = symphonia::core::io::MediaSourceStream::new( - Box::new(file), - Default::default(), - ); - let probe = symphonia::default::get_probe(); - let Ok(probed) = probe.format( - // TODO: add file extension to the hint - &symphonia::core::probe::Hint::new(), - mss, - &Default::default(), - &Default::default(), - ) else { - eprintln!("format error"); - return; - }; - let mut format = probed.format; - let Some(track) = format - .tracks() - .iter() - .find(|t| t.codec_params.codec != symphonia::core::codecs::CODEC_TYPE_NULL) - else { - eprintln!("no decodable track found"); - return; - }; - let Ok(mut decoder) = symphonia::default::get_codecs() - .make(&track.codec_params, &Default::default()) - else { - eprintln!("no decoder found"); - return; - }; - let track_id = track.id; - let Some(sample_rate) = track.codec_params.sample_rate else { - eprintln!("no sample rate"); - return; - }; - let Some(sample_rate) = NonZero::new(sample_rate) else { - eprintln!("sample rate = 0"); - return; - }; - let mut buf = Vec::new(); - // i don't know yet. after the first iteration of the loop this is set - let mut stereo: Option = None; - loop { - let packet = format.next_packet(); - let packet = match packet { - Ok(p) => p, - // this is used as a end of stream signal. don't ask me why - Err(symphonia::core::errors::Error::IoError(e)) - if e.kind() == std::io::ErrorKind::UnexpectedEof => - { - break; - } - Err(e) => { - eprintln!("decoding error: {e:?}"); - return; - } - }; - - if packet.track_id() != track_id { - continue; - } - match decoder.decode(&packet) { - Ok(audio_buf) => { - fn append_to_buf( - buf: &mut Vec, - in_buf: &symphonia::core::audio::AudioBuffer, - stereo: &mut Option, - ) where - T: symphonia::core::sample::Sample, - f32: symphonia::core::conv::FromSample, - { - use symphonia::core::{ - audio::{Channels, Signal}, - conv::FromSample, - }; - if in_buf - .spec() - .channels - .contains(Channels::FRONT_LEFT | Channels::FRONT_RIGHT) - { - // stereo + plus maybe other channels that i ignore - assert!(stereo.is_none() || *stereo == Some(true)); - *stereo = Some(true); - let left = in_buf.chan(0); - let right = in_buf.chan(1); - assert!(left.len() == right.len()); - let iter = zip(left, right).flat_map(|(l, r)| { - [f32::from_sample(*l), f32::from_sample(*r)] - }); - buf.extend(iter); - } else if in_buf.spec().channels.contains(Channels::FRONT_LEFT) - { - // assert not - assert!(stereo.is_none() || *stereo == Some(false)); - *stereo = Some(false); - buf.extend( - in_buf - .chan(0) - .iter() - .map(|sample| f32::from_sample(*sample)), - ); - } else { - eprintln!("no usable channel in sample data") - } - } - use symphonia::core::audio::AudioBufferRef; - match audio_buf { - AudioBufferRef::U8(d) => { - append_to_buf(&mut buf, &d, &mut stereo) - } - AudioBufferRef::U16(d) => { - append_to_buf(&mut buf, &d, &mut stereo) - } - AudioBufferRef::U24(d) => { - append_to_buf(&mut buf, &d, &mut stereo) - } - AudioBufferRef::U32(d) => { - append_to_buf(&mut buf, &d, &mut stereo) - } - AudioBufferRef::S8(d) => { - append_to_buf(&mut buf, &d, &mut stereo) - } - AudioBufferRef::S16(d) => { - append_to_buf(&mut buf, &d, &mut stereo) - } - AudioBufferRef::S24(d) => { - append_to_buf(&mut buf, &d, &mut stereo) - } - AudioBufferRef::S32(d) => { - append_to_buf(&mut buf, &d, &mut stereo) - } - AudioBufferRef::F32(d) => { - append_to_buf(&mut buf, &d, &mut stereo) - } - AudioBufferRef::F64(d) => { - append_to_buf(&mut buf, &d, &mut stereo) - } - } - } - Err(symphonia::core::errors::Error::DecodeError(_)) => (), - Err(_) => break, - } - } - // hopefully both of these compile to a memcopy... - let sample = if stereo.unwrap() { - Sample::new_stereo_interpolated(buf) - } else { - Sample::new_mono(buf) - }; - // TODO: get the real metadata / sane defaults / configurable - let meta = SampleMetaData { - default_volume: 32, - global_volume: 32, - default_pan: None, - vibrato_speed: 0, - vibrato_depth: 0, - vibrato_rate: 0, - vibrato_waveform: Default::default(), - sample_rate, - base_note: Note::new(64).unwrap(), - }; - // send to UI - proxy - .send_event(GlobalEvent::Page(PageEvent::SampleList( - SampleListEvent::SetSample(idx, file_name, meta), - ))) - .unwrap(); - drop(proxy); - // send to playback - let operation = SongOperation::SetSample(idx, meta, sample); - SONG_OP_SEND.get().unwrap().send(operation).await.unwrap(); - }) - .detach(); - } - // TODO: add PageUp and PageDown - - PageResponse::None - } -} diff --git a/src/ui/widgets.rs b/src/ui/widgets.rs deleted file mode 100644 index 47181ce..0000000 --- a/src/ui/widgets.rs +++ /dev/null @@ -1,138 +0,0 @@ -pub mod button; -pub mod slider; -pub mod text_in; -pub mod text_in_scroll; -pub mod toggle; -pub mod toggle_button; - -use winit::{ - event::{KeyEvent, Modifiers}, - keyboard::{Key, ModifiersState, NamedKey}, -}; - -use crate::{app::EventQueue, draw_buffer::DrawBuffer}; - -pub(crate) trait Widget { - type Response; - fn draw(&self, draw_buffer: &mut DrawBuffer, selected: bool); - /// returns a Some(usize) if the next widget gets selected - fn process_input( - &mut self, - modifiers: &Modifiers, - key_event: &KeyEvent, - events: &mut EventQueue<'_>, - ) -> WidgetResponse; -} - -#[derive(Debug)] -pub struct WidgetResponse { - pub standard: StandardResponse, - pub extra: Option, -} - -impl Default for WidgetResponse { - fn default() -> Self { - Self { - standard: StandardResponse::default(), - extra: None, - } - } -} - -impl WidgetResponse { - pub fn request_redraw() -> Self { - Self { - standard: StandardResponse::RequestRedraw, - extra: None, - } - } - - pub fn next_widget(value: usize) -> Self { - Self { - standard: StandardResponse::SwitchFocus(value), - extra: None, - } - } -} - -// SwitchFocus also has to request a redraw -#[derive(Debug, Default)] -pub enum StandardResponse { - SwitchFocus(usize), - RequestRedraw, - // GlobalEvent(GlobalEvent), - #[default] - None, -} - -impl From for WidgetResponse { - fn from(value: StandardResponse) -> Self { - Self { - standard: value, - extra: None, - } - } -} - -#[derive(Debug, Default)] -pub struct NextWidget { - pub left: Option, - pub right: Option, - pub up: Option, - pub down: Option, - pub tab: Option, - pub shift_tab: Option, -} - -impl NextWidget { - /// supposed to be called from Widgets, that own a NextWidget after catching their custom KeyEvents to pick a return - pub fn process_key_event( - &self, - key_event: &KeyEvent, - modifiers: &Modifiers, - ) -> WidgetResponse { - if !key_event.state.is_pressed() { - return WidgetResponse::default(); - } - - #[expect( - non_local_definitions, - reason = "this is only valid with these specific Option not in general" - )] - impl From> for WidgetResponse { - fn from(value: Option) -> Self { - Self { - standard: match value { - Some(num) => StandardResponse::SwitchFocus(num), - None => StandardResponse::None, - }, - extra: None, - } - } - } - - if key_event.logical_key == Key::Named(NamedKey::ArrowUp) && modifiers.state().is_empty() { - self.up.into() - } else if key_event.logical_key == Key::Named(NamedKey::ArrowDown) - && modifiers.state().is_empty() - { - self.down.into() - } else if key_event.logical_key == Key::Named(NamedKey::ArrowRight) - && modifiers.state().is_empty() - { - self.right.into() - } else if key_event.logical_key == Key::Named(NamedKey::ArrowLeft) - && modifiers.state().is_empty() - { - self.left.into() - } else if key_event.logical_key == Key::Named(NamedKey::Tab) { - if modifiers.state() == ModifiersState::SHIFT { - self.shift_tab.into() - } else { - self.tab.into() - } - } else { - WidgetResponse::default() - } - } -} diff --git a/src/ui/widgets/text_in.rs b/src/ui/widgets/text_in.rs deleted file mode 100644 index d163e39..0000000 --- a/src/ui/widgets/text_in.rs +++ /dev/null @@ -1,174 +0,0 @@ -use ascii::{AsciiChar, AsciiString}; -use font8x8::UnicodeFonts; -use winit::keyboard::{Key, NamedKey}; - -use crate::{ - app::EventQueue, - coordinates::{CharPosition, WINDOW_SIZE}, - draw_buffer::DrawBuffer, -}; - -use super::{NextWidget, StandardResponse, Widget, WidgetResponse}; - -/// text has max_len of the rect that was given, because the text_in cannot scroll -/// use text_in_scroll for that -/// i only allow Ascii characters as i can only render ascii -pub struct TextIn { - pos: CharPosition, - width: usize, - text: AsciiString, - next_widget: NextWidget, - callback: Box R + Send>, - cursor_pos: usize, -} - -impl Widget for TextIn { - type Response = R; - fn draw(&self, draw_buffer: &mut DrawBuffer, selected: bool) { - draw_buffer.draw_string_length(self.text.as_str(), self.pos, self.width, 2, 0); - // draw the cursor by overdrawing a letter - if selected { - let cursor_char_pos = self.pos + CharPosition::new(self.cursor_pos, 0); - if self.cursor_pos < self.text.len() { - draw_buffer.draw_char( - font8x8::BASIC_FONTS - .get(self.text[self.cursor_pos].into()) - .unwrap(), - cursor_char_pos, - 0, - 3, - ); - } else { - draw_buffer.draw_rect(3, cursor_char_pos.into()); - } - } - } - - fn process_input( - &mut self, - modifiers: &winit::event::Modifiers, - key_event: &winit::event::KeyEvent, - _: &mut EventQueue<'_>, - ) -> WidgetResponse { - if !key_event.state.is_pressed() { - return WidgetResponse::default(); - } - - if let Key::Character(str) = &key_event.logical_key { - let mut char_iter = str.chars(); - // why would i get a character event if there wasnt a char?? so i unwrap - let first_char = char_iter.next().unwrap(); - - if let Ok(ascii_char) = AsciiChar::from_ascii(first_char) { - self.insert_char(ascii_char); - } - // make sure i only got one char. dont know why i should get more than one - // if this ever panics switch to a loop implementation above - assert!(char_iter.next().is_none()); - return WidgetResponse::request_redraw(); - } else if key_event.logical_key == Key::Named(NamedKey::Space) { - self.insert_char(AsciiChar::Space); - return WidgetResponse::request_redraw(); - } else if key_event.logical_key == Key::Named(NamedKey::ArrowLeft) - && modifiers.state().is_empty() - { - // when moving left from 0 i dont need to redraw - if self.cursor_pos > 0 { - self.cursor_pos -= 1; - return WidgetResponse::request_redraw(); - } - } else if key_event.logical_key == Key::Named(NamedKey::ArrowRight) - && modifiers.state().is_empty() - { - // check that the cursor doesnt go away from the string - if self.text.len() > self.cursor_pos { - self.cursor_pos += 1; - return WidgetResponse::request_redraw(); - } - // backspace and delete keys - // entf on German Keyboards - } else if key_event.logical_key == Key::Named(NamedKey::Delete) { - // cant delete if im outside the text, also includes text empty - if self.cursor_pos < self.text.len() { - let _ = self.text.remove(self.cursor_pos); - return WidgetResponse { - standard: StandardResponse::RequestRedraw, - extra: Some((self.callback)(self.text.as_str())), - }; - } - } else if key_event.logical_key == Key::Named(NamedKey::Backspace) { - // super + backspace clears the string - if modifiers.state().super_key() { - self.text.clear(); - self.cursor_pos = 0; - return WidgetResponse { - standard: StandardResponse::RequestRedraw, - extra: Some((self.callback)(self.text.as_str())), - }; - // if string is empty we cant remove from it - } else if modifiers.state().is_empty() && !self.text.is_empty() { - // if cursor at position 0 backspace starts to behave like delete, no idea why original is like that - if self.cursor_pos == 0 { - let _ = self.text.remove(0); - } else { - let _ = self.text.remove(self.cursor_pos - 1); - self.cursor_pos -= 1; - } - return WidgetResponse { - standard: StandardResponse::RequestRedraw, - extra: Some((self.callback)(self.text.as_str())), - }; - } - // next widget - } else { - return self.next_widget.process_key_event(key_event, modifiers); - } - WidgetResponse::default() - } -} - -impl TextIn { - pub fn new( - pos: CharPosition, - width: usize, - next_widget: NextWidget, - cb: impl Fn(&str) -> R + Send + 'static, - ) -> Self { - assert!(pos.x() + width < WINDOW_SIZE.0); - // right and left keys are used in the widget itself. doeesnt make sense to put NextWidget there - assert!(next_widget.right.is_none()); - assert!(next_widget.left.is_none()); - - TextIn { - pos, - width, - text: AsciiString::with_capacity(width), // allows to never allocate or deallocate in TextIn - next_widget, - callback: Box::new(cb), - cursor_pos: 0, - } - } - - // not tested - pub fn set_string(&mut self, new_str: String) -> Result> { - self.text = AsciiString::from_ascii(new_str)?; - self.text.truncate(self.width); - self.cursor_pos = self.text.len(); - - Ok((self.callback)(self.text.as_str())) - } - - pub fn get_str(&self) -> &str { - self.text.as_str() - } - - fn insert_char(&mut self, char: AsciiChar) -> R { - if self.cursor_pos < self.width { - self.cursor_pos += 1; - } - self.text.insert(self.cursor_pos - 1, char); - self.text.truncate(self.width); - - (self.callback)(self.text.as_str()) - } -} diff --git a/src/ui/widgets/text_in_scroll.rs b/src/ui/widgets/text_in_scroll.rs deleted file mode 100644 index 7b76d8a..0000000 --- a/src/ui/widgets/text_in_scroll.rs +++ /dev/null @@ -1,212 +0,0 @@ -use ascii::{AsciiChar, AsciiString}; -use font8x8::UnicodeFonts; -use winit::keyboard::{Key, NamedKey}; - -use crate::{ - app::EventQueue, - coordinates::{CharPosition, WINDOW_SIZE}, - draw_buffer::DrawBuffer, - ui::widgets::StandardResponse, -}; - -use super::{NextWidget, Widget, WidgetResponse}; - -pub struct TextInScroll { - pos: CharPosition, - width: usize, - text: AsciiString, - next_widget: NextWidget, - callback: Box R>, - cursor_pos: usize, - scroll_offset: usize, -} - -impl Widget for TextInScroll { - type Response = R; - fn draw(&self, draw_buffer: &mut DrawBuffer, selected: bool) { - draw_buffer.draw_string_length( - &self.text.as_str()[self.scroll_offset..], - self.pos, - self.width, - 2, - 0, - ); - // draw the cursor by overdrawing a letter - if selected { - let cursor_char_pos = - self.pos + CharPosition::new(self.cursor_pos - self.scroll_offset, 0); - if self.cursor_pos < self.text.len() { - draw_buffer.draw_char( - font8x8::BASIC_FONTS - .get(self.text[self.cursor_pos].into()) - .unwrap(), - cursor_char_pos, - 0, - 3, - ); - } else { - draw_buffer.draw_rect(3, cursor_char_pos.into()); - } - } - } - - fn process_input( - &mut self, - modifiers: &winit::event::Modifiers, - key_event: &winit::event::KeyEvent, - _: &mut EventQueue<'_>, - ) -> WidgetResponse { - if !key_event.state.is_pressed() { - return WidgetResponse::default(); - } - - if let Key::Character(str) = &key_event.logical_key { - let mut char_iter = str.chars(); - // why would i get a character event if there wasnt a char?? so i unwrap - let first_char = char_iter.next().unwrap(); - - if let Ok(ascii_char) = AsciiChar::from_ascii(first_char) { - self.insert_char(ascii_char); - } - // make sure i only got one char. dont know why i should get more than one - // if this ever panics switch to a loop implementation above - assert!(char_iter.next().is_none()); - return WidgetResponse { - standard: StandardResponse::RequestRedraw, - extra: Some((self.callback)(self.text.as_str())), - }; - } else if key_event.logical_key == Key::Named(NamedKey::Space) { - self.insert_char(AsciiChar::Space); - return WidgetResponse::request_redraw(); - } else if key_event.logical_key == Key::Named(NamedKey::ArrowLeft) - && modifiers.state().is_empty() - { - if self.move_cursor_left() { - return WidgetResponse::request_redraw(); - } - } else if key_event.logical_key == Key::Named(NamedKey::ArrowRight) - && modifiers.state().is_empty() - { - if self.move_cursor_right() { - return WidgetResponse::request_redraw(); - } - // backspace and delete keys - // entf on German Keyboards - } else if key_event.logical_key == Key::Named(NamedKey::Delete) { - // cant delete if i'm outside the text. also includes text empty - if self.cursor_pos < self.text.len() { - let _ = self.text.remove(self.cursor_pos); - return WidgetResponse { - standard: StandardResponse::RequestRedraw, - extra: Some((self.callback)(self.text.as_str())), - }; - } - } else if key_event.logical_key == Key::Named(NamedKey::Backspace) { - // super + backspace clears the string - if modifiers.state().super_key() { - self.text.clear(); - self.cursor_pos = 0; - self.scroll_offset = 0; - return WidgetResponse { - standard: StandardResponse::RequestRedraw, - extra: Some((self.callback)(self.text.as_str())), - }; - // if string is empty we cant remove from it - } else if modifiers.state().is_empty() && !self.text.is_empty() { - // if cursor at position 0 backspace starts to behave like delete, no idea why original is like that - if self.cursor_pos == 0 { - let _ = self.text.remove(0); - } else { - let _ = self.text.remove(self.cursor_pos - 1); - self.move_cursor_left(); - } - - return WidgetResponse { - standard: StandardResponse::RequestRedraw, - extra: Some((self.callback)(self.text.as_str())), - }; - } - // next widget - } else { - return self.next_widget.process_key_event(key_event, modifiers); - } - WidgetResponse::default() - } -} - -impl TextInScroll { - pub fn new( - pos: CharPosition, - width: usize, - next_widget: NextWidget, - cb: impl Fn(&str) -> R + 'static, - ) -> Self { - assert!(pos.x() + width < WINDOW_SIZE.0); - // right and left keys are used in the widget itself. doeesnt make sense to put NextWidget there - assert!(next_widget.right.is_none()); - assert!(next_widget.left.is_none()); - - Self { - pos, - width, - text: AsciiString::new(), // size completely unknown, so i don't allocate - next_widget, - callback: Box::new(cb), - cursor_pos: 0, - scroll_offset: 0, - } - } - - pub fn set_string<'a>( - &mut self, - new_str: &'a str, - ) -> Result> { - self.text = AsciiString::from_ascii(new_str)?; - self.text.truncate(self.width); - if self.cursor_pos > self.text.len() { - self.cursor_pos = self.text.len(); - } - // never tested could be buggy - self.scroll_offset = self.text.len().saturating_sub(self.width); - - Ok((self.callback)(self.text.as_str())) - } - - fn insert_char(&mut self, char: AsciiChar) -> R { - self.text.insert(self.cursor_pos, char); - - self.move_cursor_right(); - - (self.callback)(self.text.as_str()) - } - - // returns if it moved - fn move_cursor_left(&mut self) -> bool { - if self.cursor_pos > 0 { - self.cursor_pos -= 1; - if self.cursor_pos < self.scroll_offset { - self.scroll_offset -= 1; - } - true - } else { - false - } - } - - // returns if it moved - fn move_cursor_right(&mut self) -> bool { - if self.cursor_pos < self.text.len() { - self.cursor_pos += 1; - if self.cursor_pos > self.scroll_offset + self.width { - self.scroll_offset += 1; - } - true - } else { - false - } - } - - pub fn get_str(&self) -> &str { - self.text.as_str() - } -} diff --git a/src/ui/widgets/button.rs b/src/widgets/button.rs similarity index 73% rename from src/ui/widgets/button.rs rename to src/widgets/button.rs index 53deb40..c93ca24 100644 --- a/src/ui/widgets/button.rs +++ b/src/widgets/button.rs @@ -1,23 +1,23 @@ use winit::keyboard::{Key, NamedKey}; use crate::{ - app::EventQueue, + EventQueue, coordinates::{CharPosition, CharRect}, draw_buffer::DrawBuffer, }; -use super::{NextWidget, StandardResponse, Widget, WidgetResponse}; +use super::{NextWidget, Widget, WidgetResponse}; -pub struct Button { +pub struct Button { text: &'static str, rect: CharRect, pressed: bool, next_widget: NextWidget, - callback: fn() -> R, + #[cfg(feature = "accesskit")] + node_id: accesskit::NodeId, } -impl Widget for Button { - type Response = R; +impl Widget for Button { fn draw(&self, buffer: &mut DrawBuffer, selected: bool) { self.draw_overwrite_pressed(buffer, selected, false); } @@ -27,43 +27,48 @@ impl Widget for Button { modifiers: &winit::event::Modifiers, key_event: &winit::event::KeyEvent, _: &mut EventQueue<'_>, - ) -> WidgetResponse { + ) -> WidgetResponse { if key_event.logical_key == Key::Named(NamedKey::Space) || key_event.logical_key == Key::Named(NamedKey::Enter) { if !key_event.repeat { if key_event.state.is_pressed() { self.pressed = true; - return WidgetResponse::default(); + return WidgetResponse::None; } else { - // only call the callback on a release event if the button was pressed in before - // meaning if the user pressed the key, then changed focus to another button and then releases - // no button should be triggered - let response = if self.pressed { - Some((self.callback)()) - } else { - None - }; + let pressed = self.pressed; self.pressed = false; - return WidgetResponse { - standard: StandardResponse::RequestRedraw, - extra: response, - }; + + return WidgetResponse::RequestRedraw(pressed); } } // if focus is changed stop being pressed } else { return self.next_widget.process_key_event(key_event, modifiers); } - WidgetResponse::default() + WidgetResponse::None + } + + #[cfg(feature = "accesskit")] + fn build_tree(&self, tree: &mut Vec<(accesskit::NodeId, accesskit::Node)>) { + use accesskit::{Node, Role}; + + let mut node = Node::new(Role::Button); + node.set_label(self.text); + tree.push((self.node_id, node)); } } -impl Button { +impl Button { const TOPLEFT_COLOR: u8 = 3; const BOTRIGHT_COLOR: u8 = 1; - pub fn new(text: &'static str, rect: CharRect, next_widget: NextWidget, cb: fn() -> R) -> Self { + pub fn new( + text: &'static str, + rect: CharRect, + next_widget: NextWidget, + #[cfg(feature = "accesskit")] node_id: accesskit::NodeId, + ) -> Self { // is 3 rows high, because bot and top are inclusive assert!( rect.bot() - rect.top() >= 2, @@ -72,9 +77,10 @@ impl Button { Button { text, rect, - callback: cb, pressed: false, next_widget, + #[cfg(feature = "accesskit")] + node_id, } } diff --git a/src/widgets/mod.rs b/src/widgets/mod.rs new file mode 100644 index 0000000..5e77561 --- /dev/null +++ b/src/widgets/mod.rs @@ -0,0 +1,158 @@ +pub mod button; +pub mod slider; +pub mod text_in; +// pub mod text_in_scroll; +pub mod toggle; +pub mod toggle_button; + +use winit::{ + event::{KeyEvent, Modifiers}, + keyboard::{Key, ModifiersState, NamedKey}, +}; + +use crate::{EventQueue, draw_buffer::DrawBuffer, pages::PageResponse}; + +pub(crate) trait Widget { + fn draw(&self, draw_buffer: &mut DrawBuffer, selected: bool); + /// true if the widget data was changed by the input + fn process_input( + &mut self, + modifiers: &Modifiers, + key_event: &KeyEvent, + events: &mut EventQueue<'_>, + ) -> WidgetResponse; + + #[cfg(feature = "accesskit")] + fn build_tree(&self, tree: &mut Vec<(accesskit::NodeId, accesskit::Node)>); +} + +// #[derive(Debug)] +// pub struct WidgetResponse { +// pub standard: StandardResponse, +// pub extra: Option, +// } + +// impl Default for WidgetResponse { +// fn default() -> Self { +// Self { +// standard: StandardResponse::default(), +// extra: None, +// } +// } +// } + +// impl WidgetResponse { +// pub fn request_redraw() -> Self { +// Self { +// standard: StandardResponse::RequestRedraw, +// extra: None, +// } +// } + +// pub fn next_widget(value: u8) -> Self { +// Self { +// standard: StandardResponse::SwitchFocus(value), +// extra: None, +// } +// } +// } + +// SwitchFocus also has to request a redraw +#[derive(Debug, Default, PartialEq, Eq)] +pub enum WidgetResponse { + SwitchFocus(u8), + /// true if the redraw is because the widget data was changed. + /// This could be true even if the data didn't change. This makes the implementation of some widgets easier. + /// It is still useful to keep other parts of the system in sync. + /// + /// A redraw request could otherwise be made because for example a internal cursor changed + RequestRedraw(bool), + // GlobalEvent(GlobalEvent), + #[default] + None, +} + +// impl From for WidgetResponse { +// fn from(value: StandardResponse) -> Self { +// Self { +// standard: value, +// extra: None, +// } +// } +// } + +impl WidgetResponse { + pub fn to_page_resp(self, selected: &mut u8) -> PageResponse { + match self { + WidgetResponse::SwitchFocus(s) => { + *selected = s; + PageResponse::RequestRedraw + } + WidgetResponse::RequestRedraw(_) => PageResponse::RequestRedraw, + WidgetResponse::None => PageResponse::None, + } + } + + pub fn on_change(self, f: impl FnOnce()) -> Self { + if self == WidgetResponse::RequestRedraw(true) { + f(); + } + self + } +} + +#[derive(Debug, Default)] +pub struct NextWidget { + pub left: Option, + pub right: Option, + pub up: Option, + pub down: Option, + pub tab: Option, + pub shift_tab: Option, +} + +impl NextWidget { + /// supposed to be called from Widgets, that own a NextWidget after catching their custom KeyEvents to pick a return + pub fn process_key_event(&self, key_event: &KeyEvent, modifiers: &Modifiers) -> WidgetResponse { + if !key_event.state.is_pressed() { + return WidgetResponse::None; + } + + #[expect( + non_local_definitions, + reason = "this is only valid with these specific Option not in general" + )] + impl From> for WidgetResponse { + fn from(value: Option) -> Self { + match value { + Some(num) => WidgetResponse::SwitchFocus(num), + None => WidgetResponse::None, + } + } + } + + if key_event.logical_key == Key::Named(NamedKey::ArrowUp) && modifiers.state().is_empty() { + self.up.into() + } else if key_event.logical_key == Key::Named(NamedKey::ArrowDown) + && modifiers.state().is_empty() + { + self.down.into() + } else if key_event.logical_key == Key::Named(NamedKey::ArrowRight) + && modifiers.state().is_empty() + { + self.right.into() + } else if key_event.logical_key == Key::Named(NamedKey::ArrowLeft) + && modifiers.state().is_empty() + { + self.left.into() + } else if key_event.logical_key == Key::Named(NamedKey::Tab) { + if modifiers.state() == ModifiersState::SHIFT { + self.shift_tab.into() + } else { + self.tab.into() + } + } else { + WidgetResponse::None + } + } +} diff --git a/src/ui/widgets/slider.rs b/src/widgets/slider.rs similarity index 78% rename from src/ui/widgets/slider.rs rename to src/widgets/slider.rs index ea9a06f..265e4ff 100644 --- a/src/ui/widgets/slider.rs +++ b/src/widgets/slider.rs @@ -6,13 +6,13 @@ use std::{ use winit::keyboard::{Key, NamedKey}; use crate::{ - app::{EventQueue, GlobalEvent}, - coordinates::{CharPosition, CharRect, FONT_SIZE, PixelRect, WINDOW_SIZE_CHARS}, + EventQueue, GlobalEvent, + coordinates::{CharPosition, CharRect, FONT_SIZE16, PixelRect, WINDOW_SIZE_CHARS}, + dialog::slider_dialog::SliderDialog, draw_buffer::DrawBuffer, - ui::dialog::slider_dialog::SliderDialog, }; -use super::{NextWidget, StandardResponse, Widget, WidgetResponse}; +use super::{NextWidget, Widget, WidgetResponse}; #[derive(Debug)] pub struct BoundNumber { @@ -104,23 +104,24 @@ impl BoundNumber { /// Slider needs more Space then is specified in Rect as it draws the current value with an offset of 2 right to the box. /// currently this always draws 3 chars, but this can only take values between -99 and 999. If values outside of that are needed, this implementation needs to change -pub struct Slider { +#[derive(Debug)] +pub struct Slider { number: BoundNumber, position: CharPosition, - width: usize, + width: u8, next_widget: NextWidget, dialog_return: fn(i16) -> GlobalEvent, - callback: Box R + Send>, + #[cfg(feature = "accesskit")] + access: (accesskit::NodeId, Box), } -impl Widget for Slider { - type Response = R; +impl Widget for Slider { fn draw(&self, draw_buffer: &mut DrawBuffer, selected: bool) { const BACKGROUND_COLOR: u8 = 0; const CURSOR_COLOR: u8 = 2; const CURSOR_SELECTED_COLOR: u8 = 3; - const CURSOR_WIDTH: usize = 4; + const CURSOR_WIDTH: u16 = 4; draw_buffer.draw_string( &format!("{:03}", *self.number), @@ -144,28 +145,33 @@ impl Widget for Slider { } else { // shift value scale. this is the new MAX // MIN -> MAX => 0 -> (MAX-MIN) - let num_possible_values = usize::from(MAX.abs_diff(MIN)); + let num_possible_values = MAX.abs_diff(MIN); // shift value scale as shown below // MIN -> MAX => 0 -> (MAX-MIN) - let value = usize::from(self.number.abs_diff(MIN)); + let value = self.number.abs_diff(MIN); // first + 1 makes it have a border on the left side // rest mostly stole from original source code - 1 + value * (self.width * FONT_SIZE + 1) / num_possible_values + 1 + value * (u16::from(self.width) * FONT_SIZE16 + 1) / num_possible_values }; let color = match selected { true => CURSOR_SELECTED_COLOR, false => CURSOR_COLOR, }; + let self_pixel_rect = PixelRect::from(CharRect::from(self.position)); let cursor_pixel_rect = PixelRect::new( // +1 to have a space above - (self.position.y() * FONT_SIZE) + 1, + // (self.position.y() * FONT_SIZE16) + 1, + self_pixel_rect.top() + 1, // -2 to make if have a 1. not go into the next line and 2. have a empty row below - (self.position.y() * FONT_SIZE) + (FONT_SIZE - 2), - (self.position.x() * FONT_SIZE) + cursor_pos + CURSOR_WIDTH, - (self.position.x() * FONT_SIZE) + cursor_pos, + self_pixel_rect.bot() - 2, + // (self.position.y() * FONT_SIZE16) + (FONT_SIZE16 - 2), + self_pixel_rect.left() + cursor_pos + CURSOR_WIDTH, + // (self.position.x() * FONT_SIZE16) + cursor_pos + CURSOR_WIDTH, + self_pixel_rect.left() + cursor_pos, + // (self.position.x() * FONT_SIZE16) + cursor_pos, ); draw_buffer.draw_pixel_rect(color, cursor_pixel_rect); @@ -176,7 +182,7 @@ impl Widget for Slider { modifiers: &winit::event::Modifiers, key_event: &winit::event::KeyEvent, event: &mut EventQueue<'_>, - ) -> WidgetResponse { + ) -> WidgetResponse { if !key_event.state.is_pressed() { return WidgetResponse::default(); } @@ -223,10 +229,7 @@ impl Widget for Slider { }; self.number += amount * direction; - return WidgetResponse { - standard: StandardResponse::RequestRedraw, - extra: Some((self.callback)(*self.number)), - }; + return WidgetResponse::RequestRedraw(true); } // set the value directly, by opening a pop-up } else if let Key::Character(text) = &key_event.logical_key { @@ -234,27 +237,42 @@ impl Widget for Slider { if let Some(first_char) = chars.next() { if first_char.is_ascii_digit() { let dialog = SliderDialog::new(first_char, MIN..=MAX, self.dialog_return); - event.push(GlobalEvent::OpenDialog(Box::new(|| Box::new(dialog)))); - return WidgetResponse::default(); + event.push(GlobalEvent::OpenDialog(crate::dialog::DialogEnum::Slider( + dialog, + ))); + return WidgetResponse::None; } } } else { return self.next_widget.process_key_event(key_event, modifiers); } - WidgetResponse::default() + WidgetResponse::None + } + + #[cfg(feature = "accesskit")] + fn build_tree(&self, tree: &mut Vec<(accesskit::NodeId, accesskit::Node)>) { + use accesskit::{Node, Role}; + + let mut node = Node::new(Role::Slider); + node.set_numeric_value(self.number.inner as f64); + node.set_min_numeric_value(MIN as f64); + node.set_max_numeric_value(MAX as f64); + node.set_label(self.access.1.clone()); + + tree.push((self.access.0, node)); } } -impl Slider { +impl Slider { /// next_widget left and right must be None, because they cant be called pub fn new( inital_value: i16, position: CharPosition, - width: usize, + width: u8, next_widget: NextWidget, dialog_return: fn(i16) -> GlobalEvent, - callback: impl Fn(i16) -> R + Send + 'static, + #[cfg(feature = "accesskit")] access: (accesskit::NodeId, Box), ) -> Self { assert!(MIN <= MAX, "MIN must be less than or equal to MAX"); // panic is fine, because this object only is generated with compile time values @@ -274,22 +292,16 @@ impl Slider { width, next_widget, dialog_return, - callback: Box::new(callback), + #[cfg(feature = "accesskit")] + access, } } - pub fn try_set(&mut self, value: i16) -> Result { - self.number.try_set(value).map(|_| (self.callback)(value)) + pub fn try_set(&mut self, value: i16) -> Result<(), ()> { + self.number.try_set(value) } -} -impl Debug for Slider { - fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { - f.debug_struct("Slider") - .field("number", &self.number) - .field("position", &self.position) - .field("width", &self.width) - .field("next_widget", &self.next_widget) - .finish_non_exhaustive() + pub fn get(&self) -> i16 { + self.number.inner } } diff --git a/src/widgets/text_in.rs b/src/widgets/text_in.rs new file mode 100644 index 0000000..5bac6c8 --- /dev/null +++ b/src/widgets/text_in.rs @@ -0,0 +1,363 @@ +use ascii::{AsciiChar, AsciiString}; +use font8x8::UnicodeFonts; +use winit::keyboard::{Key, NamedKey}; + +use crate::{ + EventQueue, + coordinates::{CharPosition, WINDOW_SIZE_CHARS}, + draw_buffer::DrawBuffer, +}; + +use super::{NextWidget, Widget, WidgetResponse}; + +/// text has max_len of the rect that was given, because the text_in cannot scroll +/// use text_in_scroll for that +/// i only allow Ascii characters as i can only render ascii +pub struct TextIn { + pos: CharPosition, + width: u8, + text: AsciiString, + next_widget: NextWidget, + // fixed width, so text length is also fixed + cursor_pos: u8, + #[cfg(feature = "accesskit")] + access: (accesskit::NodeId, &'static str), +} + +impl Widget for TextIn { + fn draw(&self, draw_buffer: &mut DrawBuffer, selected: bool) { + draw_buffer.draw_string_length(self.text.as_str(), self.pos, self.width, 2, 0); + // draw the cursor by overdrawing a letter + if selected { + let cursor_char_pos = self.pos + CharPosition::new(self.cursor_pos, 0); + let upos = usize::from(self.cursor_pos); + if upos < self.text.len() { + draw_buffer.draw_char( + font8x8::BASIC_FONTS.get(self.text[upos].into()).unwrap(), + cursor_char_pos, + 0, + 3, + ); + } else { + draw_buffer.draw_rect(3, cursor_char_pos.into()); + } + } + } + + fn process_input( + &mut self, + modifiers: &winit::event::Modifiers, + key_event: &winit::event::KeyEvent, + _events: &mut EventQueue<'_>, + ) -> WidgetResponse { + let mut pos = usize::from(self.cursor_pos); + let res = process_input( + &mut self.text, + self.width, + &self.next_widget, + &mut pos, + None, + modifiers, + key_event, + ); + self.cursor_pos = u8::try_from(pos).unwrap(); + res + } + + #[cfg(feature = "accesskit")] + fn build_tree(&self, tree: &mut Vec<(accesskit::NodeId, accesskit::Node)>) { + use std::iter::repeat_n; + + use accesskit::{Node, NodeId, Role, TextDirection, TextPosition, TextSelection}; + + let mut root_node = Node::new(Role::TextInput); + root_node.set_label(self.access.1); + let mut text_node = Node::new(Role::TextRun); + let text_node_id = NodeId(self.access.0.0 + 1); + text_node.set_text_direction(TextDirection::LeftToRight); + text_node.set_character_lengths(repeat_n(1, self.text.len()).collect::>()); + // text_node.set_character_widths(repeat_n(1., self.text.len()).collect::>()); + // text_node.set_character_positions( + // (0..self.text.len()) + // .map(|n| n as f32) + // .collect::>(), + // ); + text_node.set_value(self.text.as_str()); + // text_node.set_word_lengths( + // self.text + // .as_str() + // .split_whitespace() + // .map(|w| u8::try_from(w.len()).unwrap()) + // .collect::>(), + // ); + let character_index = usize::from(self.cursor_pos); + root_node.set_text_selection(TextSelection { + anchor: TextPosition { + node: text_node_id, + character_index, + }, + focus: TextPosition { + node: text_node_id, + character_index, + // character_index: self.text.as_str().len().min(self.cursor_pos + 1), + }, + }); + root_node.push_child(text_node_id); + + tree.push((self.access.0, root_node)); + tree.push((text_node_id, text_node)); + } +} + +impl TextIn { + pub fn new( + pos: CharPosition, + width: u8, + next_widget: NextWidget, + #[cfg(feature = "accesskit")] access: (accesskit::NodeId, &'static str), + ) -> Self { + assert!(pos.x() + width < WINDOW_SIZE_CHARS.0); + // right and left keys are used in the widget itself. doeesnt make sense to put NextWidget there + assert!(next_widget.right.is_none()); + assert!(next_widget.left.is_none()); + + TextIn { + pos, + width, + text: AsciiString::with_capacity(usize::from(width)), // allows to never allocate or deallocate in TextIn + next_widget, + cursor_pos: 0, + #[cfg(feature = "accesskit")] + access, + } + } + + // not tested + // TODO: can panic if the string is too long + pub fn set_string(&mut self, new_str: String) -> Result<(), ascii::FromAsciiError> { + self.text = AsciiString::from_ascii(new_str)?; + self.text.truncate(usize::from(self.width)); + self.cursor_pos = u8::try_from(self.text.len()).unwrap(); + + Ok(()) + } + + pub fn get_str(&self) -> &str { + self.text.as_str() + } +} + +pub struct TextInScroll { + pos: CharPosition, + width: u8, + text: AsciiString, + next_widget: NextWidget, + cursor_pos: usize, + scroll_offset: usize, +} + +impl Widget for TextInScroll { + fn draw(&self, draw_buffer: &mut DrawBuffer, selected: bool) { + draw_buffer.draw_string_length( + &self.text.as_str()[self.scroll_offset..], + self.pos, + self.width, + 2, + 0, + ); + + if selected { + let cursor_char_pos = self.pos + + CharPosition::new( + // this minus should make this diff be less then self.width + u8::try_from(self.cursor_pos - self.scroll_offset).unwrap(), + 0, + ); + if self.cursor_pos < self.text.len() { + draw_buffer.draw_char( + font8x8::BASIC_FONTS + .get(self.text[self.cursor_pos].into()) + .unwrap(), + cursor_char_pos, + 0, + 3, + ); + } else { + draw_buffer.draw_rect(3, cursor_char_pos.into()); + } + } + } + + fn process_input( + &mut self, + modifiers: &winit::event::Modifiers, + key_event: &winit::event::KeyEvent, + _events: &mut EventQueue<'_>, + ) -> WidgetResponse { + process_input( + &mut self.text, + self.width, + &self.next_widget, + &mut self.cursor_pos, + Some(&mut self.scroll_offset), + modifiers, + key_event, + ) + } + + #[cfg(feature = "accesskit")] + fn build_tree(&self, tree: &mut Vec<(accesskit::NodeId, accesskit::Node)>) { + todo!() + } +} + +impl TextInScroll { + pub fn new(pos: CharPosition, width: u8, next_widget: NextWidget) -> Self { + assert!(next_widget.right.is_none()); + assert!(next_widget.left.is_none()); + assert!(pos.x() + width < WINDOW_SIZE_CHARS.0); + + Self { + pos, + width, + text: AsciiString::new(), + next_widget, + cursor_pos: 0, + scroll_offset: 0, + } + } + + pub fn get_str(&self) -> &str { + self.text.as_str() + } +} + +pub fn process_input( + text: &mut AsciiString, + width: u8, + next: &NextWidget, + cursor_pos: &mut usize, + scroll_offset: Option<&mut usize>, + modifiers: &winit::event::Modifiers, + key_event: &winit::event::KeyEvent, +) -> WidgetResponse { + /// returns true if the cursor moved + fn move_cursor_left( + cursor_pos: &mut usize, + scroll_offset: Option<&mut usize>, + ) -> WidgetResponse { + if *cursor_pos > 0 { + *cursor_pos -= 1; + if let Some(scroll) = scroll_offset + && *cursor_pos < *scroll + { + *scroll -= 1; + } + // redraw, but no state change + WidgetResponse::RequestRedraw(false) + } else { + WidgetResponse::None + } + } + + /// returns true if the cursor moved + fn move_cursor_right( + text: &AsciiString, + cursor_pos: &mut usize, + width: u8, + scroll_offset: Option<&mut usize>, + ) -> WidgetResponse { + if *cursor_pos < text.len() { + *cursor_pos += 1; + if let Some(scroll) = scroll_offset + && *cursor_pos > *scroll + usize::from(width) + { + *scroll += 1; + } + // redraw, but no state change + WidgetResponse::RequestRedraw(false) + } else { + WidgetResponse::None + } + } + + fn insert_char( + text: &mut AsciiString, + width: u8, + cursor_pos: &mut usize, + char: AsciiChar, + scroll_offset: Option<&mut usize>, + ) { + if scroll_offset.is_some() { + text.insert(*cursor_pos, char); + move_cursor_right(text, cursor_pos, width, scroll_offset); + } else { + if *cursor_pos < usize::from(width) { + *cursor_pos += 1; + } + text.insert(*cursor_pos - 1, char); + text.truncate(usize::from(width)); + } + } + + if !key_event.state.is_pressed() { + return WidgetResponse::None; + } + + if let Key::Character(str) = &key_event.logical_key { + let mut char_iter = str.chars(); + let first_char = char_iter.next().unwrap(); + assert!(char_iter.next().is_none()); + + if let Ok(ascii_char) = AsciiChar::from_ascii(first_char) { + insert_char(text, width, cursor_pos, ascii_char, scroll_offset); + WidgetResponse::RequestRedraw(true) + } else { + WidgetResponse::None + } + } else if key_event.logical_key == Key::Named(NamedKey::Space) { + insert_char(text, width, cursor_pos, AsciiChar::Space, scroll_offset); + return WidgetResponse::RequestRedraw(true); + } else if key_event.logical_key == Key::Named(NamedKey::ArrowLeft) + && modifiers.state().is_empty() + { + move_cursor_left(cursor_pos, scroll_offset) + } else if key_event.logical_key == Key::Named(NamedKey::ArrowRight) + && modifiers.state().is_empty() + { + move_cursor_right(text, cursor_pos, width, scroll_offset) + // entf key on german keyboard + } else if key_event.logical_key == Key::Named(NamedKey::Delete) { + // can't delete if cursor is at the front of the text or the text is empty + if *cursor_pos < text.len() { + _ = text.remove(*cursor_pos); + WidgetResponse::RequestRedraw(true) + } else { + WidgetResponse::None + } + } else if key_event.logical_key == Key::Named(NamedKey::Backspace) { + // super + backspace clears the string + // if the text is already empty we don't need to do anything + if modifiers.state().super_key() && !text.is_empty() { + text.clear(); + *cursor_pos = 0; + if let Some(scroll) = scroll_offset { + *scroll = 0; + } + WidgetResponse::RequestRedraw(true) + } else if modifiers.state().is_empty() && !text.is_empty() { + if *cursor_pos == 0 { + _ = text.remove(0); + } else { + _ = text.remove(*cursor_pos - 1); + move_cursor_left(cursor_pos, scroll_offset); + } + WidgetResponse::RequestRedraw(true) + } else { + WidgetResponse::None + } + } else { + // next widget select + next.process_key_event(key_event, modifiers) + } +} diff --git a/src/ui/widgets/toggle.rs b/src/widgets/toggle.rs similarity index 69% rename from src/ui/widgets/toggle.rs rename to src/widgets/toggle.rs index ccf5744..8948c2b 100644 --- a/src/ui/widgets/toggle.rs +++ b/src/widgets/toggle.rs @@ -1,24 +1,22 @@ use winit::keyboard::{Key, NamedKey}; use crate::{ - app::EventQueue, - coordinates::{CharPosition, CharRect, WINDOW_SIZE}, + EventQueue, + coordinates::{CharPosition, CharRect, WINDOW_SIZE_CHARS}, draw_buffer::DrawBuffer, }; -use super::{NextWidget, StandardResponse, Widget, WidgetResponse}; +use super::{NextWidget, Widget, WidgetResponse}; -pub struct Toggle { +pub struct Toggle { pos: CharPosition, - width: usize, + width: u8, state: usize, next_widget: NextWidget, variants: &'static [(T, &'static str)], - cb: Box R>, } -impl Widget for Toggle { - type Response = R; +impl Widget for Toggle { fn draw(&self, draw_buffer: &mut DrawBuffer, selected: bool) { let str = self.variants[self.state].1; draw_buffer.draw_rect( @@ -26,7 +24,7 @@ impl Widget for Toggle { CharRect::new( self.pos.y(), self.pos.y(), - self.pos.x() + str.len(), + self.pos.x() + u8::try_from(str.len()).unwrap(), self.pos.x() + self.width, ), ); @@ -42,31 +40,32 @@ impl Widget for Toggle { modifiers: &winit::event::Modifiers, key_event: &winit::event::KeyEvent, _: &mut EventQueue<'_>, - ) -> WidgetResponse { + ) -> WidgetResponse { if key_event.logical_key == Key::Named(NamedKey::Space) && modifiers.state().is_empty() && key_event.state.is_pressed() { self.next(); - WidgetResponse { - standard: StandardResponse::RequestRedraw, - extra: Some((*self.cb)(self.variants[self.state].0)), - } + WidgetResponse::RequestRedraw(true) } else { self.next_widget.process_key_event(key_event, modifiers) } } + + #[cfg(feature = "accesskit")] + fn build_tree(&self, tree: &mut Vec<(accesskit::NodeId, accesskit::Node)>) { + todo!() + } } -impl Toggle { +impl Toggle { pub fn new( pos: CharPosition, - width: usize, + width: u8, next_widget: NextWidget, variants: &'static [(T, &'static str)], - cb: impl Fn(T) -> R + 'static, ) -> Self { - assert!(pos.x() + width < WINDOW_SIZE.0); + assert!(pos.x() + width < WINDOW_SIZE_CHARS.0); Self { pos, @@ -74,7 +73,6 @@ impl Toggle { state: 0, next_widget, variants, - cb: Box::new(cb), } } diff --git a/src/ui/widgets/toggle_button.rs b/src/widgets/toggle_button.rs similarity index 53% rename from src/ui/widgets/toggle_button.rs rename to src/widgets/toggle_button.rs index 548171a..ec4ee87 100644 --- a/src/ui/widgets/toggle_button.rs +++ b/src/widgets/toggle_button.rs @@ -1,20 +1,18 @@ use std::{cell::Cell, rc::Rc}; -use crate::{app::EventQueue, coordinates::CharRect, draw_buffer::DrawBuffer}; +use crate::{EventQueue, coordinates::CharRect, draw_buffer::DrawBuffer}; use super::{NextWidget, Widget, WidgetResponse, button::Button}; // dont need to store a callback as it gets pushed into the inner button callback -pub struct ToggleButton { - button: Button<()>, +pub struct ToggleButton { + button: Button, variant: T, - cb: fn(T) -> R, state: Rc>, } -impl Widget for ToggleButton { - type Response = R; +impl Widget for ToggleButton { fn draw(&self, draw_buffer: &mut DrawBuffer, selected: bool) { self.button .draw_overwrite_pressed(draw_buffer, selected, self.variant == self.state.get()) @@ -25,31 +23,34 @@ impl Widget for ToggleButton { modifiers: &winit::event::Modifiers, key_event: &winit::event::KeyEvent, event: &mut EventQueue<'_>, - ) -> WidgetResponse { - let WidgetResponse { standard, extra } = - self.button.process_input(modifiers, key_event, event); - let extra = extra.map(|_| { - self.state.set(self.variant); - (self.cb)(self.variant) - }); - WidgetResponse { standard, extra } + ) -> WidgetResponse { + self.button.process_input(modifiers, key_event, event) + } + + #[cfg(feature = "accesskit")] + fn build_tree(&self, tree: &mut Vec<(accesskit::NodeId, accesskit::Node)>) { + todo!() } } -impl ToggleButton { +impl ToggleButton { pub fn new( text: &'static str, rect: CharRect, next_widget: NextWidget, variant: T, state: Rc>, - cb: fn(T) -> R, ) -> Self { - let button = Button::new(text, rect, next_widget, || ()); + let button = Button::new( + text, + rect, + next_widget, + #[cfg(feature = "accesskit")] + todo!(), + ); Self { button, variant, - cb, state, } }