diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..40d9aca --- /dev/null +++ b/.gitignore @@ -0,0 +1,2 @@ +/target +/.idea \ No newline at end of file diff --git a/.vscode/tasks.json b/.vscode/tasks.json new file mode 100644 index 0000000..f6f31f7 --- /dev/null +++ b/.vscode/tasks.json @@ -0,0 +1,21 @@ +{ + "version": "2.0.0", + "tasks": [ + { + "type": "cargo", + "command": "run", + "problemMatcher": [ + "$rustc" + ], + "args": [ + "-p", + "nodio-app" + ], + "label": "rust: cargo run -p nodio-app", + "group": { + "kind": "build", + "isDefault": true + } + } + ] +} \ No newline at end of file diff --git a/Cargo.lock b/Cargo.lock new file mode 100644 index 0000000..a077ac5 --- /dev/null +++ b/Cargo.lock @@ -0,0 +1,2073 @@ +# This file is automatically @generated by Cargo. +# It is not intended for manual editing. +version = 3 + +[[package]] +name = "ab_glyph" +version = "0.2.13" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "61caed9aec6daeee1ea38ccf5fb225e4f96c1eeead1b4a5c267324a63cf02326" +dependencies = [ + "ab_glyph_rasterizer", + "owned_ttf_parser", +] + +[[package]] +name = "ab_glyph_rasterizer" +version = "0.1.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a13739d7177fbd22bb0ed28badfff9f372f8bef46c863db4e1c6248f6b223b6e" + +[[package]] +name = "addr2line" +version = "0.17.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b9ecd88a8c8378ca913a680cd98f0f13ac67383d35993f86c90a70e3f137816b" +dependencies = [ + "gimli", +] + +[[package]] +name = "adler" +version = "1.0.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f26201604c87b1e01bd3d98f8d5d9a8fcbb815e8cedb41ffccbeb4bf593a35fe" + +[[package]] +name = "ahash" +version = "0.7.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fcb51a0695d8f838b1ee009b3fbf66bda078cd64590202a864a8f3e8c4315c47" +dependencies = [ + "getrandom", + "once_cell", + "serde", + "version_check", +] + +[[package]] +name = "aho-corasick" +version = "0.7.18" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1e37cfd5e7657ada45f742d6e99ca5788580b5c529dc78faf11ece6dc702656f" +dependencies = [ + "memchr", +] + +[[package]] +name = "android_glue" +version = "0.2.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "000444226fcff248f2bc4c7625be32c63caccfecc2723a2b9f78a7487a49c407" + +[[package]] +name = "arboard" +version = "2.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6045ca509e4abacde2b884ac4618a51d0c017b5d85a3ee84a7226eb33b3154a9" +dependencies = [ + "clipboard-win", + "log", + "objc", + "objc-foundation", + "objc_id", + "once_cell", + "parking_lot 0.12.0", + "thiserror", + "winapi", + "x11rb", +] + +[[package]] +name = "atomic_refcell" +version = "0.1.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "73b5e5f48b927f04e952dedc932f31995a65a0bf65ec971c74436e51bf6e970d" + +[[package]] +name = "atty" +version = "0.2.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d9b39be18770d11421cdb1b9947a45dd3f37e93092cbf377614828a319d5fee8" +dependencies = [ + "hermit-abi", + "libc", + "winapi", +] + +[[package]] +name = "autocfg" +version = "1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d468802bab17cbc0cc575e9b053f41e72aa36bfa6b7f55e3529ffa43161b97fa" + +[[package]] +name = "backtrace" +version = "0.3.65" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "11a17d453482a265fd5f8479f2a3f405566e6ca627837aaddb85af8b1ab8ef61" +dependencies = [ + "addr2line", + "cc", + "cfg-if 1.0.0", + "libc", + "miniz_oxide", + "object", + "rustc-demangle", +] + +[[package]] +name = "base64" +version = "0.13.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "904dfeac50f3cdaba28fc6f57fdcddb75f49ed61346676a78c4ffe55877802fd" + +[[package]] +name = "bitflags" +version = "1.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bef38d45163c2f1dde094a7dfd33ccf595c92905c8f8f4fdc18d06fb1037718a" + +[[package]] +name = "block" +version = "0.1.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0d8c1fef690941d3e7788d328517591fecc684c084084702d6ff1641e993699a" + +[[package]] +name = "bumpalo" +version = "3.9.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a4a45a46ab1f2412e53d3a0ade76ffad2025804294569aae387231a0cd6e0899" + +[[package]] +name = "bytemuck" +version = "1.9.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cdead85bdec19c194affaeeb670c0e41fe23de31459efd1c174d049269cf02cc" +dependencies = [ + "bytemuck_derive", +] + +[[package]] +name = "bytemuck_derive" +version = "1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "562e382481975bc61d11275ac5e62a19abd00b0547d99516a415336f183dcd0e" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "bytes" +version = "1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c4872d67bab6358e59559027aa3b9157c53d9358c51423c17554809a8858e0f8" + +[[package]] +name = "calloop" +version = "0.9.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bf2eec61efe56aa1e813f5126959296933cf0700030e4314786c48779a66ab82" +dependencies = [ + "log", + "nix", +] + +[[package]] +name = "cc" +version = "1.0.72" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "22a9137b95ea06864e018375b72adfb7db6e6f68cfc8df5a04d00288050485ee" + +[[package]] +name = "cesu8" +version = "1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6d43a04d8753f35258c91f8ec639f792891f748a1edbd759cf1dcea3382ad83c" + +[[package]] +name = "cfg-if" +version = "0.1.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4785bdd1c96b2a846b2bd7cc02e86b6b3dbf14e7e53446c4f54c92a361040822" + +[[package]] +name = "cfg-if" +version = "1.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "baf1de4339761588bc0619e3cbc0120ee582ebb74b53b4efbf79117bd2da40fd" + +[[package]] +name = "cgl" +version = "0.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0ced0551234e87afee12411d535648dd89d2e7f34c78b753395567aff3d447ff" +dependencies = [ + "libc", +] + +[[package]] +name = "clipboard-win" +version = "4.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2f3e1238132dc01f081e1cbb9dace14e5ef4c3a51ee244bd982275fb514605db" +dependencies = [ + "error-code", + "str-buf", + "winapi", +] + +[[package]] +name = "cocoa" +version = "0.24.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6f63902e9223530efb4e26ccd0cf55ec30d592d3b42e21a28defc42a9586e832" +dependencies = [ + "bitflags", + "block", + "cocoa-foundation", + "core-foundation 0.9.3", + "core-graphics 0.22.3", + "foreign-types", + "libc", + "objc", +] + +[[package]] +name = "cocoa-foundation" +version = "0.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7ade49b65d560ca58c403a479bb396592b155c0185eada742ee323d1d68d6318" +dependencies = [ + "bitflags", + "block", + "core-foundation 0.9.3", + "core-graphics-types", + "foreign-types", + "libc", + "objc", +] + +[[package]] +name = "combine" +version = "4.6.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2a604e93b79d1808327a6fca85a6f2d69de66461e7620f5a4cbf5fb4d1d7c948" +dependencies = [ + "bytes", + "memchr", +] + +[[package]] +name = "core-foundation" +version = "0.7.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "57d24c7a13c43e870e37c1556b74555437870a04514f7685f5b354e090567171" +dependencies = [ + "core-foundation-sys 0.7.0", + "libc", +] + +[[package]] +name = "core-foundation" +version = "0.9.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "194a7a9e6de53fa55116934067c844d9d749312f75c6f6d0980e8c252f8c2146" +dependencies = [ + "core-foundation-sys 0.8.3", + "libc", +] + +[[package]] +name = "core-foundation-sys" +version = "0.7.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b3a71ab494c0b5b860bdc8407ae08978052417070c2ced38573a9157ad75b8ac" + +[[package]] +name = "core-foundation-sys" +version = "0.8.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5827cebf4670468b8772dd191856768aedcb1b0278a04f989f7766351917b9dc" + +[[package]] +name = "core-graphics" +version = "0.19.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b3889374e6ea6ab25dba90bb5d96202f61108058361f6dc72e8b03e6f8bbe923" +dependencies = [ + "bitflags", + "core-foundation 0.7.0", + "foreign-types", + "libc", +] + +[[package]] +name = "core-graphics" +version = "0.22.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2581bbab3b8ffc6fcbd550bf46c355135d16e9ff2a6ea032ad6b9bf1d7efe4fb" +dependencies = [ + "bitflags", + "core-foundation 0.9.3", + "core-graphics-types", + "foreign-types", + "libc", +] + +[[package]] +name = "core-graphics-types" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3a68b68b3446082644c91ac778bf50cd4104bfb002b5a6a7c44cca5a2c70788b" +dependencies = [ + "bitflags", + "core-foundation 0.9.3", + "foreign-types", + "libc", +] + +[[package]] +name = "core-video-sys" +version = "0.1.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "34ecad23610ad9757664d644e369246edde1803fcb43ed72876565098a5d3828" +dependencies = [ + "cfg-if 0.1.10", + "core-foundation-sys 0.7.0", + "core-graphics 0.19.2", + "libc", + "objc", +] + +[[package]] +name = "cty" +version = "0.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b365fabc795046672053e29c954733ec3b05e4be654ab130fe8f1f94d7051f35" + +[[package]] +name = "darling" +version = "0.13.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d0d720b8683f8dd83c65155f0530560cba68cd2bf395f6513a483caee57ff7f4" +dependencies = [ + "darling_core", + "darling_macro", +] + +[[package]] +name = "darling_core" +version = "0.13.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7a340f241d2ceed1deb47ae36c4144b2707ec7dd0b649f894cb39bb595986324" +dependencies = [ + "fnv", + "ident_case", + "proc-macro2", + "quote", + "strsim", + "syn", +] + +[[package]] +name = "darling_macro" +version = "0.13.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "72c41b3b7352feb3211a0d743dc5700a4e3b60f51bd2b368892d1e0f9a95f44b" +dependencies = [ + "darling_core", + "quote", + "syn", +] + +[[package]] +name = "derivative" +version = "2.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fcc3dd5e9e9c0b295d6e1e4d811fb6f157d5ffd784b8d202fc62eac8035a770b" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "directories-next" +version = "2.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "339ee130d97a610ea5a5872d2bbb130fdf68884ff09d3028b81bec8a1ac23bbc" +dependencies = [ + "cfg-if 1.0.0", + "dirs-sys-next", +] + +[[package]] +name = "dirs-sys-next" +version = "0.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4ebda144c4fe02d1f7ea1a7d9641b6fc6b580adcfa024ae48797ecdeb6825b4d" +dependencies = [ + "libc", + "redox_users", + "winapi", +] + +[[package]] +name = "dispatch" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bd0c93bb4b0c6d9b77f4435b0ae98c24d17f1c45b2ff844c6151a07256ca923b" + +[[package]] +name = "dlib" +version = "0.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ac1b7517328c04c2aa68422fc60a41b92208182142ed04a25879c26c8f878794" +dependencies = [ + "libloading", +] + +[[package]] +name = "downcast-rs" +version = "1.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9ea835d29036a4087793836fa931b08837ad5e957da9e23886b29586fb9b6650" + +[[package]] +name = "eframe" +version = "0.18.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "53fa97a8188c36261ea162e625dbb23599f67b60777b462b834fe38161b81dce" +dependencies = [ + "bytemuck", + "directories-next", + "egui", + "egui-winit", + "egui_glow", + "glow", + "glutin", + "js-sys", + "percent-encoding", + "ron", + "serde", + "tracing", + "wasm-bindgen", + "wasm-bindgen-futures", + "web-sys", + "winit", +] + +[[package]] +name = "egui" +version = "0.18.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "eb095a8b9feb9b7ff8f00b6776dffcef059538a3f4a91238e03c900e9c9ad9a2" +dependencies = [ + "ahash", + "epaint", + "nohash-hasher", + "ron", + "serde", + "tracing", +] + +[[package]] +name = "egui-toast" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "584414aff3b4345d86c50e68f96d76a824324c8d3260b70b11da1dd0dcdd8f26" +dependencies = [ + "egui", +] + +[[package]] +name = "egui-winit" +version = "0.18.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b040afd583fd95a9b9578d4399214a13d948ed26bc0ff7cc0104502501f34e68" +dependencies = [ + "arboard", + "egui", + "instant", + "serde", + "tracing", + "webbrowser", + "winit", +] + +[[package]] +name = "egui_glow" +version = "0.18.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "af43ed7ec7199907ab5853c3bb3883ae1e741ab540aa127a798a60b7bdb906f1" +dependencies = [ + "bytemuck", + "egui", + "glow", + "memoffset", + "tracing", + "wasm-bindgen", + "web-sys", +] + +[[package]] +name = "emath" +version = "0.18.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c223f58c7e38abe1770f367b969f1b3fbd4704b67666bcb65dbb1adb0980ba72" +dependencies = [ + "bytemuck", + "serde", +] + +[[package]] +name = "env_logger" +version = "0.7.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "44533bbbb3bb3c1fa17d9f2e4e38bbbaf8396ba82193c4cb1b6445d711445d36" +dependencies = [ + "atty", + "humantime", + "log", + "regex", + "termcolor", +] + +[[package]] +name = "epaint" +version = "0.18.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0c29567088888e8ac3e8f61bbb2ddc820207ebb8d69eefde5bcefa06d65e4e89" +dependencies = [ + "ab_glyph", + "ahash", + "atomic_refcell", + "bytemuck", + "emath", + "nohash-hasher", + "parking_lot 0.12.0", + "serde", +] + +[[package]] +name = "error-code" +version = "2.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "64f18991e7bf11e7ffee451b5318b5c1a73c52d0d0ada6e5a3017c8c1ced6a21" +dependencies = [ + "libc", + "str-buf", +] + +[[package]] +name = "fixedbitset" +version = "0.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "279fb028e20b3c4c320317955b77c5e0c9701f05a1d309905d6fc702cdc5053e" + +[[package]] +name = "fnv" +version = "1.0.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3f9eec918d3f24069decb9af1554cad7c880e2da24a9afd88aca000531ab82c1" + +[[package]] +name = "foreign-types" +version = "0.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f6f339eb8adc052cd2ca78910fda869aefa38d22d5cb648e6485e4d3fc06f3b1" +dependencies = [ + "foreign-types-shared", +] + +[[package]] +name = "foreign-types-shared" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "00b0228411908ca8685dba7fc2cdd70ec9990a6e753e89b6ac91a84c40fbaf4b" + +[[package]] +name = "form_urlencoded" +version = "1.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5fc25a87fa4fd2094bffb06925852034d90a17f0d1e05197d4956d3555752191" +dependencies = [ + "matches", + "percent-encoding", +] + +[[package]] +name = "gethostname" +version = "0.2.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c1ebd34e35c46e00bb73e81363248d627782724609fe1b6396f553f68fe3862e" +dependencies = [ + "libc", + "winapi", +] + +[[package]] +name = "getrandom" +version = "0.2.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "418d37c8b1d42553c93648be529cb70f920d3baf8ef469b74b9638df426e0b4c" +dependencies = [ + "cfg-if 1.0.0", + "libc", + "wasi", +] + +[[package]] +name = "gimli" +version = "0.26.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "78cc372d058dcf6d5ecd98510e7fbc9e5aec4d21de70f65fea8fecebcd881bd4" + +[[package]] +name = "gl_generator" +version = "0.14.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1a95dfc23a2b4a9a2f5ab41d194f8bfda3cabec42af4e39f08c339eb2a0c124d" +dependencies = [ + "khronos_api", + "log", + "xml-rs", +] + +[[package]] +name = "glow" +version = "0.11.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d8bd5877156a19b8ac83a29b2306fe20537429d318f3ff0a1a2119f8d9c61919" +dependencies = [ + "js-sys", + "slotmap", + "wasm-bindgen", + "web-sys", +] + +[[package]] +name = "glutin" +version = "0.28.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "00ea9dbe544bc8a657c4c4a798c2d16cd01b549820e47657297549d28371f6d2" +dependencies = [ + "android_glue", + "cgl", + "cocoa", + "core-foundation 0.9.3", + "glutin_egl_sys", + "glutin_emscripten_sys", + "glutin_gles2_sys", + "glutin_glx_sys", + "glutin_wgl_sys", + "lazy_static", + "libloading", + "log", + "objc", + "osmesa-sys", + "parking_lot 0.11.2", + "wayland-client", + "wayland-egl", + "winapi", + "winit", +] + +[[package]] +name = "glutin_egl_sys" +version = "0.1.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2abb6aa55523480c4adc5a56bbaa249992e2dddb2fc63dc96e04a3355364c211" +dependencies = [ + "gl_generator", + "winapi", +] + +[[package]] +name = "glutin_emscripten_sys" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "80de4146df76e8a6c32b03007bc764ff3249dcaeb4f675d68a06caf1bac363f1" + +[[package]] +name = "glutin_gles2_sys" +version = "0.1.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e8094e708b730a7c8a1954f4f8a31880af00eb8a1c5b5bf85d28a0a3c6d69103" +dependencies = [ + "gl_generator", + "objc", +] + +[[package]] +name = "glutin_glx_sys" +version = "0.1.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7e393c8fc02b807459410429150e9c4faffdb312d59b8c038566173c81991351" +dependencies = [ + "gl_generator", + "x11-dl", +] + +[[package]] +name = "glutin_wgl_sys" +version = "0.1.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3da5951a1569dbab865c6f2a863efafff193a93caf05538d193e9e3816d21696" +dependencies = [ + "gl_generator", +] + +[[package]] +name = "hashbrown" +version = "0.11.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ab5ef0d4909ef3724cc8cce6ccc8572c5c817592e9285f5464f8e86f8bd3726e" + +[[package]] +name = "hermit-abi" +version = "0.1.19" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "62b467343b94ba476dcb2500d242dadbb39557df889310ac77c5d99100aaac33" +dependencies = [ + "libc", +] + +[[package]] +name = "humantime" +version = "1.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "df004cfca50ef23c36850aaaa59ad52cc70d0e90243c3c7737a4dd32dc7a3c4f" +dependencies = [ + "quick-error", +] + +[[package]] +name = "ident_case" +version = "1.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b9e0384b61958566e926dc50660321d12159025e767c18e043daf26b70104c39" + +[[package]] +name = "idna" +version = "0.2.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "418a0a6fab821475f634efe3ccc45c013f742efe03d853e8d3355d5cb850ecf8" +dependencies = [ + "matches", + "unicode-bidi", + "unicode-normalization", +] + +[[package]] +name = "indexmap" +version = "1.8.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0f647032dfaa1f8b6dc29bd3edb7bbef4861b8b8007ebb118d6db284fd59f6ee" +dependencies = [ + "autocfg", + "hashbrown", +] + +[[package]] +name = "instant" +version = "0.1.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7a5bbe824c507c5da5956355e86a746d82e0e1464f65d862cc5e71da70e94b2c" +dependencies = [ + "cfg-if 1.0.0", + "js-sys", + "wasm-bindgen", + "web-sys", +] + +[[package]] +name = "itoa" +version = "1.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1aab8fc367588b89dcee83ab0fd66b72b50b72fa1904d7095045ace2b0c81c35" + +[[package]] +name = "jni" +version = "0.19.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c6df18c2e3db7e453d3c6ac5b3e9d5182664d28788126d39b91f2d1e22b017ec" +dependencies = [ + "cesu8", + "combine", + "jni-sys", + "log", + "thiserror", + "walkdir", +] + +[[package]] +name = "jni-sys" +version = "0.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8eaf4bc02d17cbdd7ff4c7438cafcdf7fb9a4613313ad11b4f8fefe7d3fa0130" + +[[package]] +name = "js-sys" +version = "0.3.56" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a38fc24e30fd564ce974c02bf1d337caddff65be6cc4735a1f7eab22a7440f04" +dependencies = [ + "wasm-bindgen", +] + +[[package]] +name = "khronos_api" +version = "3.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e2db585e1d738fc771bf08a151420d3ed193d9d895a36df7f6f8a9456b911ddc" + +[[package]] +name = "lazy_static" +version = "1.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e2abad23fbc42b3700f2f279844dc832adb2b2eb069b2df918f455c4e18cc646" + +[[package]] +name = "libc" +version = "0.2.117" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e74d72e0f9b65b5b4ca49a346af3976df0f9c61d550727f349ecd559f251a26c" + +[[package]] +name = "libloading" +version = "0.7.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "efbc0f03f9a775e9f6aed295c6a1ba2253c5757a9e03d55c6caa46a681abcddd" +dependencies = [ + "cfg-if 1.0.0", + "winapi", +] + +[[package]] +name = "lock_api" +version = "0.4.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "88943dd7ef4a2e5a4bfa2753aaab3013e34ce2533d1996fb18ef591e315e2b3b" +dependencies = [ + "scopeguard", +] + +[[package]] +name = "log" +version = "0.4.17" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "abb12e687cfb44aa40f41fc3978ef76448f9b6038cad6aef4259d3c095a2382e" +dependencies = [ + "cfg-if 1.0.0", +] + +[[package]] +name = "malloc_buf" +version = "0.0.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "62bb907fe88d54d8d9ce32a3cceab4218ed2f6b7d35617cafe9adf84e43919cb" +dependencies = [ + "libc", +] + +[[package]] +name = "matches" +version = "0.1.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a3e378b66a060d48947b590737b30a1be76706c8dd7b8ba0f2fe3989c68a853f" + +[[package]] +name = "memchr" +version = "2.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "308cc39be01b73d0d18f82a0e7b2a3df85245f84af96fdddc5d202d27e47b86a" + +[[package]] +name = "memmap2" +version = "0.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "00b6c2ebff6180198788f5db08d7ce3bc1d0b617176678831a7510825973e357" +dependencies = [ + "libc", +] + +[[package]] +name = "memoffset" +version = "0.6.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5aa361d4faea93603064a027415f07bd8e1d5c88c9fbf68bf56a285428fd79ce" +dependencies = [ + "autocfg", +] + +[[package]] +name = "minimal-lexical" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "68354c5c6bd36d73ff3feceb05efa59b6acb7626617f4962be322a825e61f79a" + +[[package]] +name = "miniz_oxide" +version = "0.5.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d2b29bd4bc3f33391105ebee3589c19197c4271e3e5a9ec9bfe8127eeff8f082" +dependencies = [ + "adler", +] + +[[package]] +name = "mio" +version = "0.8.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ba272f85fa0b41fc91872be579b3bbe0f56b792aa361a380eb669469f68dafb2" +dependencies = [ + "libc", + "log", + "miow", + "ntapi", + "winapi", +] + +[[package]] +name = "miow" +version = "0.3.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b9f1c5b025cda876f66ef43a113f91ebc9f4ccef34843000e0adf6ebbab84e21" +dependencies = [ + "winapi", +] + +[[package]] +name = "ndk" +version = "0.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "96d868f654c72e75f8687572699cdabe755f03effbb62542768e995d5b8d699d" +dependencies = [ + "bitflags", + "jni-sys", + "ndk-sys", + "num_enum", + "thiserror", +] + +[[package]] +name = "ndk-glue" +version = "0.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fc291b8de2095cba8dab7cf381bf582ff4c17a09acf854c32e46545b08085d28" +dependencies = [ + "lazy_static", + "libc", + "log", + "ndk", + "ndk-macro", + "ndk-sys", +] + +[[package]] +name = "ndk-macro" +version = "0.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0df7ac00c4672f9d5aece54ee3347520b7e20f158656c7db2e6de01902eb7a6c" +dependencies = [ + "darling", + "proc-macro-crate", + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "ndk-sys" +version = "0.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e1bcdd74c20ad5d95aacd60ef9ba40fdf77f767051040541df557b7a9b2a2121" + +[[package]] +name = "nix" +version = "0.22.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e4916f159ed8e5de0082076562152a76b7a1f64a01fd9d1e0fea002c37624faf" +dependencies = [ + "bitflags", + "cc", + "cfg-if 1.0.0", + "libc", + "memoffset", +] + +[[package]] +name = "nodio" +version = "0.1.0" + +[[package]] +name = "nodio-api" +version = "0.1.0" +dependencies = [ + "nodio-core", + "nodio-win32", + "parking_lot 0.12.0", +] + +[[package]] +name = "nodio-app" +version = "0.1.0" +dependencies = [ + "eframe", + "egui-toast", + "indexmap", + "log", + "nodio-api", + "nodio-core", + "nodio-gui-nodes", + "parking_lot 0.12.0", + "pretty_env_logger", + "serde_json", +] + +[[package]] +name = "nodio-core" +version = "0.1.0" +dependencies = [ + "serde", + "serde_json", + "uuid", +] + +[[package]] +name = "nodio-gui-nodes" +version = "0.1.0" +dependencies = [ + "derivative", + "egui", + "indexmap", + "log", + "uuid", +] + +[[package]] +name = "nodio-win32" +version = "0.1.0" +dependencies = [ + "log", + "nodio-core", + "notify-thread", + "parking_lot 0.12.0", + "pollster", + "widestring 1.0.0-beta.1", + "windows", +] + +[[package]] +name = "nohash-hasher" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2bf50223579dc7cdcfb3bfcacf7069ff68243f8c363f62ffa99cf000a6b9c451" + +[[package]] +name = "nom" +version = "7.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1b1d11e1ef389c76fe5b81bcaf2ea32cf88b62bc494e19f493d0b30e7a930109" +dependencies = [ + "memchr", + "minimal-lexical", + "version_check", +] + +[[package]] +name = "notify-thread" +version = "0.1.0" +source = "git+https://github.com/urholaukkarinen/notify-thread.git#babe494889f65542500261d2fe394e95a072954a" + +[[package]] +name = "ntapi" +version = "0.3.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c28774a7fd2fbb4f0babd8237ce554b73af68021b5f695a3cebd6c59bac0980f" +dependencies = [ + "winapi", +] + +[[package]] +name = "num_enum" +version = "0.5.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "720d3ea1055e4e4574c0c0b0f8c3fd4f24c4cdaf465948206dea090b57b526ad" +dependencies = [ + "num_enum_derive", +] + +[[package]] +name = "num_enum_derive" +version = "0.5.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0d992b768490d7fe0d8586d9b5745f6c49f557da6d81dc982b1d167ad4edbb21" +dependencies = [ + "proc-macro-crate", + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "objc" +version = "0.2.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "915b1b472bc21c53464d6c8461c9d3af805ba1ef837e1cac254428f4a77177b1" +dependencies = [ + "malloc_buf", +] + +[[package]] +name = "objc-foundation" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1add1b659e36c9607c7aab864a76c7a4c2760cd0cd2e120f3fb8b952c7e22bf9" +dependencies = [ + "block", + "objc", + "objc_id", +] + +[[package]] +name = "objc_id" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c92d4ddb4bd7b50d730c215ff871754d0da6b2178849f8a2a2ab69712d0c073b" +dependencies = [ + "objc", +] + +[[package]] +name = "object" +version = "0.28.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e42c982f2d955fac81dd7e1d0e1426a7d702acd9c98d19ab01083a6a0328c424" +dependencies = [ + "memchr", +] + +[[package]] +name = "once_cell" +version = "1.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "da32515d9f6e6e489d7bc9d84c71b060db7247dc035bbe44eac88cf87486d8d5" + +[[package]] +name = "osmesa-sys" +version = "0.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "88cfece6e95d2e717e0872a7f53a8684712ad13822a7979bc760b9c77ec0013b" +dependencies = [ + "shared_library", +] + +[[package]] +name = "owned_ttf_parser" +version = "0.14.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4ef05f2882a8b3e7acc10c153ade2631f7bfc8ce00d2bf3fb8f4e9d2ae6ea5c3" +dependencies = [ + "ttf-parser", +] + +[[package]] +name = "parking_lot" +version = "0.11.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7d17b78036a60663b797adeaee46f5c9dfebb86948d1255007a1d6be0271ff99" +dependencies = [ + "instant", + "lock_api", + "parking_lot_core 0.8.5", +] + +[[package]] +name = "parking_lot" +version = "0.12.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "87f5ec2493a61ac0506c0f4199f99070cbe83857b0337006a30f3e6719b8ef58" +dependencies = [ + "lock_api", + "parking_lot_core 0.9.3", +] + +[[package]] +name = "parking_lot_core" +version = "0.8.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d76e8e1493bcac0d2766c42737f34458f1c8c50c0d23bcb24ea953affb273216" +dependencies = [ + "cfg-if 1.0.0", + "instant", + "libc", + "redox_syscall", + "smallvec", + "winapi", +] + +[[package]] +name = "parking_lot_core" +version = "0.9.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "09a279cbf25cb0757810394fbc1e359949b59e348145c643a939a525692e6929" +dependencies = [ + "backtrace", + "cfg-if 1.0.0", + "libc", + "petgraph", + "redox_syscall", + "smallvec", + "thread-id", + "windows-sys", +] + +[[package]] +name = "percent-encoding" +version = "2.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d4fd5641d01c8f18a23da7b6fe29298ff4b55afcccdf78973b24cf3175fee32e" + +[[package]] +name = "petgraph" +version = "0.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4a13a2fa9d0b63e5f22328828741e523766fff0ee9e779316902290dff3f824f" +dependencies = [ + "fixedbitset", + "indexmap", +] + +[[package]] +name = "pin-project-lite" +version = "0.2.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e0a7ae3ac2f1173085d398531c705756c94a4c56843785df85a60c1a0afac116" + +[[package]] +name = "pkg-config" +version = "0.3.24" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "58893f751c9b0412871a09abd62ecd2a00298c6c83befa223ef98c52aef40cbe" + +[[package]] +name = "pollster" +version = "0.2.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5da3b0203fd7ee5720aa0b5e790b591aa5d3f41c3ed2c34a3a393382198af2f7" + +[[package]] +name = "pretty_env_logger" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "926d36b9553851b8b0005f1275891b392ee4d2d833852c417ed025477350fb9d" +dependencies = [ + "env_logger", + "log", +] + +[[package]] +name = "proc-macro-crate" +version = "1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1ebace6889caf889b4d3f76becee12e90353f2b8c7d875534a71e5742f8f6f83" +dependencies = [ + "thiserror", + "toml", +] + +[[package]] +name = "proc-macro2" +version = "1.0.36" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c7342d5883fbccae1cc37a2353b09c87c9b0f3afd73f5fb9bba687a1f733b029" +dependencies = [ + "unicode-xid", +] + +[[package]] +name = "quick-error" +version = "1.2.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a1d01941d82fa2ab50be1e79e6714289dd7cde78eba4c074bc5a4374f650dfe0" + +[[package]] +name = "quote" +version = "1.0.15" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "864d3e96a899863136fc6e99f3d7cae289dafe43bf2c5ac19b70df7210c0a145" +dependencies = [ + "proc-macro2", +] + +[[package]] +name = "raw-window-handle" +version = "0.4.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fba75eee94a9d5273a68c9e1e105d9cffe1ef700532325788389e5a83e2522b7" +dependencies = [ + "cty", +] + +[[package]] +name = "redox_syscall" +version = "0.2.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8383f39639269cde97d255a32bdb68c047337295414940c68bdd30c2e13203ff" +dependencies = [ + "bitflags", +] + +[[package]] +name = "redox_users" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "528532f3d801c87aec9def2add9ca802fe569e44a544afe633765267840abe64" +dependencies = [ + "getrandom", + "redox_syscall", +] + +[[package]] +name = "regex" +version = "1.5.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d07a8629359eb56f1e2fb1652bb04212c072a87ba68546a04065d525673ac461" +dependencies = [ + "aho-corasick", + "memchr", + "regex-syntax", +] + +[[package]] +name = "regex-syntax" +version = "0.6.25" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f497285884f3fcff424ffc933e56d7cbca511def0c9831a7f9b5f6153e3cc89b" + +[[package]] +name = "ron" +version = "0.7.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1b861ecaade43ac97886a512b360d01d66be9f41f3c61088b42cedf92e03d678" +dependencies = [ + "base64", + "bitflags", + "serde", +] + +[[package]] +name = "rustc-demangle" +version = "0.1.21" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7ef03e0a2b150c7a90d01faf6254c9c48a41e95fb2a8c2ac1c6f0d2b9aefc342" + +[[package]] +name = "ryu" +version = "1.0.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "73b4b750c782965c211b42f022f59af1fbceabdd026623714f104152f1ec149f" + +[[package]] +name = "same-file" +version = "1.0.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "93fc1dc3aaa9bfed95e02e6eadabb4baf7e3078b0bd1b4d7b6b0b68378900502" +dependencies = [ + "winapi-util", +] + +[[package]] +name = "scoped-tls" +version = "1.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ea6a9290e3c9cf0f18145ef7ffa62d68ee0bf5fcd651017e586dc7fd5da448c2" + +[[package]] +name = "scopeguard" +version = "1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d29ab0c6d3fc0ee92fe66e2d99f700eab17a8d57d1c1d3b748380fb20baa78cd" + +[[package]] +name = "serde" +version = "1.0.136" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ce31e24b01e1e524df96f1c2fdd054405f8d7376249a5110886fb4b658484789" +dependencies = [ + "serde_derive", +] + +[[package]] +name = "serde_derive" +version = "1.0.136" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "08597e7152fcd306f41838ed3e37be9eaeed2b61c42e2117266a554fab4662f9" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "serde_json" +version = "1.0.81" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9b7ce2b32a1aed03c558dc61a5cd328f15aff2dbc17daad8fb8af04d2100e15c" +dependencies = [ + "itoa", + "ryu", + "serde", +] + +[[package]] +name = "shared_library" +version = "0.1.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5a9e7e0f2bfae24d8a5b5a66c5b257a83c7412304311512a0c054cd5e619da11" +dependencies = [ + "lazy_static", + "libc", +] + +[[package]] +name = "slotmap" +version = "1.0.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e1e08e261d0e8f5c43123b7adf3e4ca1690d655377ac93a03b2c9d3e98de1342" +dependencies = [ + "version_check", +] + +[[package]] +name = "smallvec" +version = "1.8.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f2dd574626839106c320a323308629dcb1acfc96e32a8cba364ddc61ac23ee83" + +[[package]] +name = "smithay-client-toolkit" +version = "0.15.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1325f292209cee78d5035530932422a30aa4c8fda1a16593ac083c1de211e68a" +dependencies = [ + "bitflags", + "calloop", + "dlib", + "lazy_static", + "log", + "memmap2", + "nix", + "pkg-config", + "wayland-client", + "wayland-cursor", + "wayland-protocols", +] + +[[package]] +name = "str-buf" +version = "1.0.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d44a3643b4ff9caf57abcee9c2c621d6c03d9135e0d8b589bd9afb5992cb176a" + +[[package]] +name = "strsim" +version = "0.10.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "73473c0e59e6d5812c5dfe2a064a6444949f089e20eec9a2e5506596494e4623" + +[[package]] +name = "syn" +version = "1.0.86" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8a65b3f4ffa0092e9887669db0eae07941f023991ab58ea44da8fe8e2d511c6b" +dependencies = [ + "proc-macro2", + "quote", + "unicode-xid", +] + +[[package]] +name = "termcolor" +version = "1.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2dfed899f0eb03f32ee8c6a0aabdb8a7949659e3466561fc0adf54e26d88c5f4" +dependencies = [ + "winapi-util", +] + +[[package]] +name = "thiserror" +version = "1.0.30" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "854babe52e4df1653706b98fcfc05843010039b406875930a70e4d9644e5c417" +dependencies = [ + "thiserror-impl", +] + +[[package]] +name = "thiserror-impl" +version = "1.0.30" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "aa32fd3f627f367fe16f893e2597ae3c05020f8bba2666a4e6ea73d377e5714b" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "thread-id" +version = "4.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5fdfe0627923f7411a43ec9ec9c39c3a9b4151be313e0922042581fb6c9b717f" +dependencies = [ + "libc", + "redox_syscall", + "winapi", +] + +[[package]] +name = "tinyvec" +version = "1.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "87cc5ceb3875bb20c2890005a4e226a4651264a5c75edb2421b52861a0a0cb50" +dependencies = [ + "tinyvec_macros", +] + +[[package]] +name = "tinyvec_macros" +version = "0.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cda74da7e1a664f795bb1f8a87ec406fb89a02522cf6e50620d016add6dbbf5c" + +[[package]] +name = "toml" +version = "0.5.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a31142970826733df8241ef35dc040ef98c679ab14d7c3e54d827099b3acecaa" +dependencies = [ + "serde", +] + +[[package]] +name = "tracing" +version = "0.1.34" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5d0ecdcb44a79f0fe9844f0c4f33a342cbcbb5117de8001e6ba0dc2351327d09" +dependencies = [ + "cfg-if 1.0.0", + "pin-project-lite", + "tracing-attributes", + "tracing-core", +] + +[[package]] +name = "tracing-attributes" +version = "0.1.21" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cc6b8ad3567499f98a1db7a752b07a7c8c7c7c34c332ec00effb2b0027974b7c" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "tracing-core" +version = "0.1.26" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f54c8ca710e81886d498c2fd3331b56c93aa248d49de2222ad2742247c60072f" +dependencies = [ + "lazy_static", +] + +[[package]] +name = "ttf-parser" +version = "0.14.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4ccbe8381883510b6a2d8f1e32905bddd178c11caef8083086d0c0c9ab0ac281" + +[[package]] +name = "unicode-bidi" +version = "0.3.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "099b7128301d285f79ddd55b9a83d5e6b9e97c92e0ea0daebee7263e932de992" + +[[package]] +name = "unicode-normalization" +version = "0.1.19" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d54590932941a9e9266f0832deed84ebe1bf2e4c9e4a3554d393d18f5e854bf9" +dependencies = [ + "tinyvec", +] + +[[package]] +name = "unicode-xid" +version = "0.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8ccb82d61f80a663efe1f787a51b16b5a51e3314d6ac365b08639f52387b33f3" + +[[package]] +name = "url" +version = "2.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a507c383b2d33b5fc35d1861e77e6b383d158b2da5e14fe51b83dfedf6fd578c" +dependencies = [ + "form_urlencoded", + "idna", + "matches", + "percent-encoding", +] + +[[package]] +name = "uuid" +version = "1.0.0-alpha.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bb3ab47baa004111b323696c6eaa2752e7356f7f77cf6b6dc7a2087368ce1ca4" +dependencies = [ + "getrandom", + "serde", +] + +[[package]] +name = "version_check" +version = "0.9.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "49874b5167b65d7193b8aba1567f5c7d93d001cafc34600cee003eda787e483f" + +[[package]] +name = "walkdir" +version = "2.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "808cf2735cd4b6866113f648b791c6adc5714537bc222d9347bb203386ffda56" +dependencies = [ + "same-file", + "winapi", + "winapi-util", +] + +[[package]] +name = "wasi" +version = "0.10.2+wasi-snapshot-preview1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fd6fbd9a79829dd1ad0cc20627bf1ed606756a7f77edff7b66b7064f9cb327c6" + +[[package]] +name = "wasm-bindgen" +version = "0.2.79" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "25f1af7423d8588a3d840681122e72e6a24ddbcb3f0ec385cac0d12d24256c06" +dependencies = [ + "cfg-if 1.0.0", + "wasm-bindgen-macro", +] + +[[package]] +name = "wasm-bindgen-backend" +version = "0.2.79" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8b21c0df030f5a177f3cba22e9bc4322695ec43e7257d865302900290bcdedca" +dependencies = [ + "bumpalo", + "lazy_static", + "log", + "proc-macro2", + "quote", + "syn", + "wasm-bindgen-shared", +] + +[[package]] +name = "wasm-bindgen-futures" +version = "0.4.29" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2eb6ec270a31b1d3c7e266b999739109abce8b6c87e4b31fcfcd788b65267395" +dependencies = [ + "cfg-if 1.0.0", + "js-sys", + "wasm-bindgen", + "web-sys", +] + +[[package]] +name = "wasm-bindgen-macro" +version = "0.2.79" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2f4203d69e40a52ee523b2529a773d5ffc1dc0071801c87b3d270b471b80ed01" +dependencies = [ + "quote", + "wasm-bindgen-macro-support", +] + +[[package]] +name = "wasm-bindgen-macro-support" +version = "0.2.79" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bfa8a30d46208db204854cadbb5d4baf5fcf8071ba5bf48190c3e59937962ebc" +dependencies = [ + "proc-macro2", + "quote", + "syn", + "wasm-bindgen-backend", + "wasm-bindgen-shared", +] + +[[package]] +name = "wasm-bindgen-shared" +version = "0.2.79" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3d958d035c4438e28c70e4321a2911302f10135ce78a9c7834c0cab4123d06a2" + +[[package]] +name = "wayland-client" +version = "0.29.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "91223460e73257f697d9e23d401279123d36039a3f7a449e983f123292d4458f" +dependencies = [ + "bitflags", + "downcast-rs", + "libc", + "nix", + "scoped-tls", + "wayland-commons", + "wayland-scanner", + "wayland-sys", +] + +[[package]] +name = "wayland-commons" +version = "0.29.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "94f6e5e340d7c13490eca867898c4cec5af56c27a5ffe5c80c6fc4708e22d33e" +dependencies = [ + "nix", + "once_cell", + "smallvec", + "wayland-sys", +] + +[[package]] +name = "wayland-cursor" +version = "0.29.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c52758f13d5e7861fc83d942d3d99bf270c83269575e52ac29e5b73cb956a6bd" +dependencies = [ + "nix", + "wayland-client", + "xcursor", +] + +[[package]] +name = "wayland-egl" +version = "0.29.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "83281d69ee162b59031c666385e93bde4039ec553b90c4191cdb128ceea29a3a" +dependencies = [ + "wayland-client", + "wayland-sys", +] + +[[package]] +name = "wayland-protocols" +version = "0.29.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "60147ae23303402e41fe034f74fb2c35ad0780ee88a1c40ac09a3be1e7465741" +dependencies = [ + "bitflags", + "wayland-client", + "wayland-commons", + "wayland-scanner", +] + +[[package]] +name = "wayland-scanner" +version = "0.29.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "39a1ed3143f7a143187156a2ab52742e89dac33245ba505c17224df48939f9e0" +dependencies = [ + "proc-macro2", + "quote", + "xml-rs", +] + +[[package]] +name = "wayland-sys" +version = "0.29.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d9341df79a8975679188e37dab3889bfa57c44ac2cb6da166f519a81cbe452d4" +dependencies = [ + "dlib", + "lazy_static", + "pkg-config", +] + +[[package]] +name = "web-sys" +version = "0.3.56" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c060b319f29dd25724f09a2ba1418f142f539b2be99fbf4d2d5a8f7330afb8eb" +dependencies = [ + "js-sys", + "wasm-bindgen", +] + +[[package]] +name = "webbrowser" +version = "0.7.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fc6a3cffdb686fbb24d9fb8f03a213803277ed2300f11026a3afe1f108dc021b" +dependencies = [ + "jni", + "ndk-glue", + "url", + "web-sys", + "widestring 0.5.1", + "winapi", +] + +[[package]] +name = "widestring" +version = "0.5.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "17882f045410753661207383517a6f62ec3dbeb6a4ed2acce01f0728238d1983" + +[[package]] +name = "widestring" +version = "1.0.0-beta.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e6f1efe828a707edf85994a4501734ac1c1b9d244cfcf4de235f11c4125ace8f" + +[[package]] +name = "winapi" +version = "0.3.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5c839a674fcd7a98952e593242ea400abe93992746761e38641405d28b00f419" +dependencies = [ + "winapi-i686-pc-windows-gnu", + "winapi-x86_64-pc-windows-gnu", +] + +[[package]] +name = "winapi-i686-pc-windows-gnu" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ac3b87c63620426dd9b991e5ce0329eff545bccbbb34f3be09ff6fb6ab51b7b6" + +[[package]] +name = "winapi-util" +version = "0.1.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "70ec6ce85bb158151cae5e5c87f95a8e97d2c0c4b001223f33a334e3ce5de178" +dependencies = [ + "winapi", +] + +[[package]] +name = "winapi-wsapoll" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "44c17110f57155602a80dca10be03852116403c9ff3cd25b079d666f2aa3df6e" +dependencies = [ + "winapi", +] + +[[package]] +name = "winapi-x86_64-pc-windows-gnu" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "712e227841d057c1ee1cd2fb22fa7e5a5461ae8e48fa2ca79ec42cfc1931183f" + +[[package]] +name = "windows" +version = "0.37.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "57b543186b344cc61c85b5aab0d2e3adf4e0f99bc076eff9aa5927bcc0b8a647" +dependencies = [ + "windows-implement", + "windows-interface", + "windows_aarch64_msvc 0.37.0", + "windows_i686_gnu 0.37.0", + "windows_i686_msvc 0.37.0", + "windows_x86_64_gnu 0.37.0", + "windows_x86_64_msvc 0.37.0", +] + +[[package]] +name = "windows-implement" +version = "0.37.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "67a1062e555f7d9d66fd1130ed4f7c6ec41a47529ee0850cd0e926d95b26bb14" +dependencies = [ + "syn", + "windows-tokens", +] + +[[package]] +name = "windows-interface" +version = "0.37.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e426420090dc12090b97fdbd2ce080265d1430e3776e36029e0a38f0f85cd925" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "windows-sys" +version = "0.36.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ea04155a16a59f9eab786fe12a4a450e75cdb175f9e0d80da1e17db09f55b8d2" +dependencies = [ + "windows_aarch64_msvc 0.36.1", + "windows_i686_gnu 0.36.1", + "windows_i686_msvc 0.36.1", + "windows_x86_64_gnu 0.36.1", + "windows_x86_64_msvc 0.36.1", +] + +[[package]] +name = "windows-tokens" +version = "0.37.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3263d25f1170419995b78ff10c06b949e8a986c35c208dc24333c64753a87169" + +[[package]] +name = "windows_aarch64_msvc" +version = "0.36.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9bb8c3fd39ade2d67e9874ac4f3db21f0d710bee00fe7cab16949ec184eeaa47" + +[[package]] +name = "windows_aarch64_msvc" +version = "0.37.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2623277cb2d1c216ba3b578c0f3cf9cdebeddb6e66b1b218bb33596ea7769c3a" + +[[package]] +name = "windows_i686_gnu" +version = "0.36.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "180e6ccf01daf4c426b846dfc66db1fc518f074baa793aa7d9b9aaeffad6a3b6" + +[[package]] +name = "windows_i686_gnu" +version = "0.37.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d3925fd0b0b804730d44d4b6278c50f9699703ec49bcd628020f46f4ba07d9e1" + +[[package]] +name = "windows_i686_msvc" +version = "0.36.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e2e7917148b2812d1eeafaeb22a97e4813dfa60a3f8f78ebe204bcc88f12f024" + +[[package]] +name = "windows_i686_msvc" +version = "0.37.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ce907ac74fe331b524c1298683efbf598bb031bc84d5e274db2083696d07c57c" + +[[package]] +name = "windows_x86_64_gnu" +version = "0.36.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4dcd171b8776c41b97521e5da127a2d86ad280114807d0b2ab1e462bc764d9e1" + +[[package]] +name = "windows_x86_64_gnu" +version = "0.37.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2babfba0828f2e6b32457d5341427dcbb577ceef556273229959ac23a10af33d" + +[[package]] +name = "windows_x86_64_msvc" +version = "0.36.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c811ca4a8c853ef420abd8592ba53ddbbac90410fab6903b3e79972a631f7680" + +[[package]] +name = "windows_x86_64_msvc" +version = "0.37.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f4dd6dc7df2d84cf7b33822ed5b86318fb1781948e9663bacd047fc9dd52259d" + +[[package]] +name = "winit" +version = "0.26.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9b43cc931d58b99461188607efd7acb2a093e65fc621f54cad78517a6063e73a" +dependencies = [ + "bitflags", + "cocoa", + "core-foundation 0.9.3", + "core-graphics 0.22.3", + "core-video-sys", + "dispatch", + "instant", + "lazy_static", + "libc", + "log", + "mio", + "ndk", + "ndk-glue", + "ndk-sys", + "objc", + "parking_lot 0.11.2", + "percent-encoding", + "raw-window-handle", + "smithay-client-toolkit", + "wasm-bindgen", + "wayland-client", + "wayland-protocols", + "web-sys", + "winapi", + "x11-dl", +] + +[[package]] +name = "x11-dl" +version = "2.19.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ea26926b4ce81a6f5d9d0f3a0bc401e5a37c6ae14a1bfaa8ff6099ca80038c59" +dependencies = [ + "lazy_static", + "libc", + "pkg-config", +] + +[[package]] +name = "x11rb" +version = "0.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6e99be55648b3ae2a52342f9a870c0e138709a3493261ce9b469afe6e4df6d8a" +dependencies = [ + "gethostname", + "nix", + "winapi", + "winapi-wsapoll", +] + +[[package]] +name = "xcursor" +version = "0.3.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "463705a63313cd4301184381c5e8042f0a7e9b4bb63653f216311d4ae74690b7" +dependencies = [ + "nom", +] + +[[package]] +name = "xml-rs" +version = "0.8.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d2d7d3948613f75c98fd9328cfdcc45acc4d360655289d0a7d4ec931392200a3" diff --git a/Cargo.toml b/Cargo.toml new file mode 100644 index 0000000..044f1de --- /dev/null +++ b/Cargo.toml @@ -0,0 +1,7 @@ +[package] +name = "nodio" +version = "0.1.0" +edition = "2021" + +[workspace] +members = ["crates/*"] \ No newline at end of file diff --git a/LICENSE b/LICENSE new file mode 100644 index 0000000..42de4c8 --- /dev/null +++ b/LICENSE @@ -0,0 +1,21 @@ +MIT License + +Copyright (c) 2022 Urho Laukkarinen + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. \ No newline at end of file diff --git a/README.md b/README.md new file mode 100644 index 0000000..47c18f0 --- /dev/null +++ b/README.md @@ -0,0 +1,27 @@ +# Nodio + +![Screenshot of Nodio](docs/screenshot.png) + +> **Note** +> This project is still very much a work in progress and experimental. Use with caution. + +Nodio is a node-based audio routing application that allows you to route audio from applications and input +devices to multiple output devices. + +At the moment Nodio only works on **Windows 10 (ver. 21H1 and later)** and **Windows 11**, but Mac and Linux support may come *later*™. + +## Usage +``` +cargo run -p nodio-app +``` + +## Features + +* Route audio from an application to one or several output devices. On Windows this works by switching the application's +default audio endpoint to the first connected output device, and using software loopback recording for the rest. + +* Route audio from an input device (e.g. a microphone) to one output device. On Windows this works by using Windows' +"Listen to this device" feature. + +* The nodes and connections are automatically saved. If the application is restarted, the previous layout is loaded +and applied. \ No newline at end of file diff --git a/crates/nodio-api/Cargo.toml b/crates/nodio-api/Cargo.toml new file mode 100644 index 0000000..824f643 --- /dev/null +++ b/crates/nodio-api/Cargo.toml @@ -0,0 +1,11 @@ +[package] +name = "nodio-api" +version = "0.1.0" +edition = "2021" + +[dependencies] +nodio-core = { path = "../nodio-core" } +parking_lot = "0.12.0" + +[target.'cfg(windows)'.dependencies] +nodio-win32 = { path = "../nodio-win32" } \ No newline at end of file diff --git a/crates/nodio-api/src/lib.rs b/crates/nodio-api/src/lib.rs new file mode 100644 index 0000000..2e13d8c --- /dev/null +++ b/crates/nodio-api/src/lib.rs @@ -0,0 +1,11 @@ +#![deny(clippy::all)] +use nodio_core::Context; +use parking_lot::RwLock; +use std::sync::Arc; + +#[cfg(target_os = "windows")] +use nodio_win32::Win32Context as PlatformContext; + +pub fn create_nodio_context() -> Arc> { + PlatformContext::new() +} diff --git a/crates/nodio-app/Cargo.toml b/crates/nodio-app/Cargo.toml new file mode 100644 index 0000000..64b4735 --- /dev/null +++ b/crates/nodio-app/Cargo.toml @@ -0,0 +1,18 @@ +[package] +name = "nodio-app" +version = "0.1.0" +edition = "2021" + +[dependencies] +nodio-core = { path = "../nodio-core" } +nodio-api = { path = "../nodio-api" } +nodio-gui-nodes = { path = "../nodio-gui-nodes" } + +eframe = { version = "0.18.0", features = ["persistence"] } +egui-toast = "0.2.0" + +pretty_env_logger = "0.4.0" +log = "0.4.17" +indexmap = "1.8.1" +serde_json = "1.0.81" +parking_lot = "0.12.0" \ No newline at end of file diff --git a/crates/nodio-app/fonts/Lato-Regular.ttf b/crates/nodio-app/fonts/Lato-Regular.ttf new file mode 100644 index 0000000..bb2e887 Binary files /dev/null and b/crates/nodio-app/fonts/Lato-Regular.ttf differ diff --git a/crates/nodio-app/src/main.rs b/crates/nodio-app/src/main.rs new file mode 100644 index 0000000..4d6257c --- /dev/null +++ b/crates/nodio-app/src/main.rs @@ -0,0 +1,431 @@ +#![deny(clippy::all)] +use std::ops::Sub; +use std::sync::Arc; +use std::time::Duration; + +use eframe::{egui, App, CreationContext, Frame, NativeOptions, Storage}; +use egui::{pos2, Color32, FontData, FontDefinitions, FontFamily, RichText, Style, Widget}; +use egui_toast::Toasts; +use indexmap::IndexMap; +use log::{debug, warn}; +use parking_lot::RwLock; + +use nodio_api::create_nodio_context; +use nodio_core::{Context, DeviceInfo, ProcessInfo, Uuid}; +use nodio_core::{Node, NodeKind}; +use nodio_gui_nodes::{AttributeFlags, Context as NodeContext, LinkArgs, PinArgs}; +use slider::VolumeSlider; + +use crate::egui::{Direction, Pos2, Response, Ui}; + +mod slider; + +fn main() { + pretty_env_logger::init(); + + eframe::run_native( + "Nodio", + NativeOptions { + ..Default::default() + }, + Box::new(setup_app), + ); +} + +fn setup_app(setup_ctx: &CreationContext) -> Box { + let mut app = MyApp::default(); + + let mut style = Style::default(); + style.visuals.override_text_color = Some(Color32::from_rgb(225, 225, 225)); + style.visuals.widgets.noninteractive.bg_fill = Color32::from_rgba_unmultiplied(50, 50, 50, 255); + setup_ctx.egui_ctx.set_style(style); + + let mut fonts = FontDefinitions::default(); + fonts.font_data.insert( + "custom".to_owned(), + FontData::from_static(include_bytes!("../fonts/Lato-Regular.ttf")), + ); + fonts + .families + .get_mut(&FontFamily::Proportional) + .unwrap() + .insert(0, "custom".to_owned()); + fonts + .families + .get_mut(&FontFamily::Monospace) + .unwrap() + .push("custom".to_owned()); + + setup_ctx.egui_ctx.set_fonts(fonts); + + if let Some(nodes_json) = setup_ctx + .storage + .and_then(|storage| storage.get_string("nodes")) + { + let mut ctx = app.ctx.write(); + for node in serde_json::from_str::>(&nodes_json).unwrap_or_default() { + ctx.add_node(node); + } + } + + if let Some(links_json) = setup_ctx + .storage + .and_then(|storage| storage.get_string("links")) + { + let mut ctx = app.ctx.write(); + for (id, start, end) in serde_json::from_str::>(&links_json).unwrap_or_default() { + if app.ui_links.insert(id, (start, end)).is_none() { + ctx.connect_node(start, end).ok(); + } + } + } + + Box::new(app) +} + +#[derive(Copy, Clone)] +enum ContextMenuKind { + Node(Uuid), + Editor, +} + +struct MyApp { + ctx: Arc>, + node_ctx: NodeContext, + /// Links between nodes (id, (start -> end)) + ui_links: IndexMap, + context_menu_kind: Option, + detached_link: Option<(Uuid, Uuid)>, + + should_save: bool, +} + +impl Default for MyApp { + fn default() -> Self { + Self { + ctx: create_nodio_context(), + node_ctx: NodeContext::default(), + ui_links: IndexMap::new(), + context_menu_kind: None, + detached_link: None, + should_save: false, + } + } +} + +impl MyApp { + fn interact_and_draw(&mut self, ui_ctx: &egui::Context, ui: &mut Ui) { + let node_count = self.ctx.read().nodes().len(); + + let mut toasts = Toasts::new(ui_ctx) + .anchor( + ui_ctx + .available_rect() + .max + .sub(Pos2::new(10.0, 10.0)) + .to_pos2(), + ) + .align_to_end(true) + .direction(Direction::BottomUp); + + self.node_ctx.begin_frame(ui); + + for node_idx in 0..node_count { + let Node { + id: node_id, + kind: node_kind, + volume: mut node_volume, + active: node_active, + present: node_present, + peak_values: node_peak_values, + display_name: node_display_name, + pos: node_pos, + .. + } = self.ctx.read().nodes().get(node_idx).cloned().unwrap(); + + let pin_args = match node_kind { + NodeKind::Application | NodeKind::InputDevice => PinArgs::default(), + NodeKind::OutputDevice => PinArgs { + flags: Some(AttributeFlags::EnableLinkDetachWithDragClick as _), + ..Default::default() + }, + }; + + let header_contents = |ui: &mut Ui| { + ui.vertical_centered(|ui| { + ui.add_enabled_ui(node_present, move |ui| { + ui.label(format!( + "{}{}", + node_display_name, + if node_active { " 🔉" } else { "" } + )) + }); + }); + }; + + let attr_contents = { + let ctx = self.ctx.clone(); + move |ui: &mut Ui| { + ui.vertical(|ui| { + ui.add_enabled_ui(node_present, |ui| { + ui.spacing_mut().slider_width = 130.0; + + if VolumeSlider::new(&mut node_volume, node_peak_values) + .ui(ui) + .changed() + { + ctx.write().set_volume(node_id, node_volume); + } + }); + }) + .response + } + }; + + let mut node = self + .node_ctx + .add_node(node_id) + .with_origin(pos2(node_pos.0, node_pos.1)) + .with_header(header_contents); + + match node_kind { + NodeKind::Application | NodeKind::InputDevice => { + node.with_output_attribute(node_id, pin_args, attr_contents); + } + NodeKind::OutputDevice => { + node.with_input_attribute(node_id, pin_args, attr_contents); + } + } + + node.show(ui); + } + + for (&id, &(start, end)) in self.ui_links.iter() { + self.node_ctx + .add_link(id, start, end, LinkArgs::default(), ui); + } + + let nodes_response = self.node_ctx.end_frame(ui); + + self.context_menu(nodes_response); + + if let Some(id) = self.node_ctx.detached_link() { + debug!("link detached: {}", id); + + if let Some((from, to)) = self.ui_links.remove(&id) { + self.ctx.write().disconnect_node(from, to); + self.detached_link = Some((from, to)); + } + } + + if let Some(id) = self.node_ctx.dropped_link() { + debug!("link dropped: {}", id); + + self.should_save = true; + self.detached_link = None; + } + + if let Some((start, end, from_snap)) = self.node_ctx.created_link() { + debug!("link created: {}, ({} to {})", start, end, from_snap); + + match self.ctx.write().connect_node(start, end) { + Ok(()) => { + self.ui_links.retain(|_, link| *link != (start, end)); + self.ui_links.insert(Uuid::new_v4(), (start, end)); + } + Err(err) => { + warn!("Failed to connect nodes: {}", err); + + toasts.error(err.to_string(), Duration::from_secs(10)); + + if let Some((from, to)) = self.detached_link.take() { + self.ui_links.insert(Uuid::new_v4(), (from, to)); + } + } + } + + self.should_save = true; + } + + if node_count == 0 { + ui.centered_and_justified(|ui| { + ui.label( + RichText::new("Right-click anywhere to add nodes") + .heading() + .color(ui.visuals().widgets.inactive.text_color()), + ); + }); + } + + if ui.input().key_pressed(egui::Key::Delete) { + self.remove_selected_nodes(); + } + + toasts.show(); + } + + fn context_menu(&mut self, nodes_response: Response) { + let context_menu_kind = self + .context_menu_kind + .take() + .or_else(|| self.node_ctx.hovered_node().map(ContextMenuKind::Node)) + .unwrap_or(ContextMenuKind::Editor); + + nodes_response.context_menu(|ui| { + self.context_menu_kind = Some(context_menu_kind); + + match context_menu_kind { + ContextMenuKind::Node(node_id) => self.node_context_menu_items(ui, node_id), + ContextMenuKind::Editor => self.editor_context_menu_items(ui), + } + }); + } + + fn node_context_menu_items(&mut self, ui: &mut Ui, node_id: Uuid) { + if ui.button("Remove").clicked() { + self.ctx.write().remove_node(node_id); + self.ui_links + .retain(|_, (start, end)| *start != node_id && *end != node_id); + + // Remove other nodes too, when multiple nodes selected + self.remove_selected_nodes(); + + ui.close_menu(); + } + } + + fn remove_selected_nodes(&mut self) { + for &node_id in self.node_ctx.get_selected_nodes() { + self.ctx.write().remove_node(node_id); + self.ui_links + .retain(|_, (start, end)| *start != node_id && *end != node_id); + } + } + + fn editor_context_menu_items(&mut self, ui: &mut Ui) { + let mut added_node = None; + + let menu_pos = ui + .add_enabled_ui(false, |ui| ui.label("Add node")) + .response + .rect + .min; + + ui.menu_button("Application", |ui| { + for process in self.ctx.read().application_processes() { + Self::application_node_button(&mut added_node, menu_pos, ui, process); + } + }); + + ui.menu_button("Input device", |ui| { + for device in self.ctx.read().input_devices() { + Self::device_node_button( + &mut added_node, + menu_pos, + ui, + device, + NodeKind::InputDevice, + ); + } + }); + + ui.menu_button("Output device", |ui| { + for device in self.ctx.read().output_devices() { + Self::device_node_button( + &mut added_node, + menu_pos, + ui, + device, + NodeKind::OutputDevice, + ); + } + }); + + if let Some(node) = added_node { + self.ctx.write().add_node(node); + self.should_save = true; + } + } + + fn application_node_button( + added_node: &mut Option, + menu_pos: Pos2, + ui: &mut Ui, + process: ProcessInfo, + ) { + if egui::Button::new(&process.display_name) + .wrap(false) + .ui(ui) + .clicked() + { + added_node.replace(Node { + kind: NodeKind::Application, + display_name: process.display_name, + filename: process.filename, + pos: (menu_pos.x, menu_pos.y), + process_id: Some(process.pid), + ..Default::default() + }); + ui.close_menu(); + } + } + + fn device_node_button( + added_node: &mut Option, + menu_pos: Pos2, + ui: &mut Ui, + device: DeviceInfo, + node_kind: NodeKind, + ) { + if egui::Button::new(&device.name).wrap(false).ui(ui).clicked() { + added_node.replace(Node { + id: device.id, + kind: node_kind, + display_name: device.name, + pos: (menu_pos.x, menu_pos.y), + ..Default::default() + }); + ui.close_menu(); + } + } +} + +impl App for MyApp { + fn update(&mut self, ui_ctx: &egui::Context, _frame: &mut Frame) { + egui::CentralPanel::default() + .frame(egui::Frame::none()) + .show(ui_ctx, |ui| self.interact_and_draw(ui_ctx, ui)); + + ui_ctx.request_repaint(); + } + + fn save(&mut self, storage: &mut dyn Storage) { + debug!("Saving state"); + + self.should_save = false; + + let mut nodes = self.ctx.read().nodes().to_vec(); + for node in nodes.iter_mut() { + if let Some(pos) = self.node_ctx.node_pos(node.id) { + node.pos = (pos.x, pos.y); + } + } + + let links: Vec<(Uuid, Uuid, Uuid)> = self + .ui_links + .iter() + .map(|(id, (start, end))| (*id, *start, *end)) + .collect::<_>(); + + storage.set_string("nodes", serde_json::to_string_pretty(&nodes).unwrap()); + storage.set_string("links", serde_json::to_string_pretty(&links).unwrap()); + } + + fn auto_save_interval(&self) -> Duration { + if self.should_save { + Duration::from_secs(0) + } else { + Duration::from_secs(30) + } + } +} diff --git a/crates/nodio-app/src/slider.rs b/crates/nodio-app/src/slider.rs new file mode 100644 index 0000000..a8a8afa --- /dev/null +++ b/crates/nodio-app/src/slider.rs @@ -0,0 +1,688 @@ +#![allow(dead_code)] +///! Modified version of [egui::widgets::Slider] for audio channel visualization +use eframe::egui; +use egui::*; +use std::ops::RangeInclusive; + +#[derive(Clone)] +struct SliderSpec { + logarithmic: bool, + /// For logarithmic sliders, the smallest positive value we are interested in. + /// 1 for integer sliders, maybe 1e-6 for others. + smallest_positive: f64, + /// For logarithmic sliders, the largest positive value we are interested in + /// before the slider switches to `INFINITY`, if that is the higher end. + /// Default: INFINITY. + largest_finite: f64, +} + +/// Combined into one function (rather than two) to make it easier +/// for the borrow checker. +type GetSetValue<'a> = Box) -> f64>; + +fn get(get_set_value: &mut GetSetValue<'_>) -> f64 { + (get_set_value)(None) +} + +fn set(get_set_value: &mut GetSetValue<'_>, value: f64) { + (get_set_value)(Some(value)); +} + +/// Specifies the orientation of a [`VolumeSlider`]. +pub enum SliderOrientation { + Horizontal, + Vertical, +} + +#[must_use = "You should put this widget in an ui with `ui.add(widget);`"] +pub struct VolumeSlider<'a> { + get_set_value: GetSetValue<'a>, + range: RangeInclusive, + spec: SliderSpec, + clamp_to_range: bool, + smart_aim: bool, + show_value: bool, + orientation: SliderOrientation, + prefix: String, + suffix: String, + text: String, + text_color: Option, + min_decimals: usize, + max_decimals: Option, + peak_values: (f32, f32), +} + +impl<'a> VolumeSlider<'a> { + pub fn new(value: &'a mut f32, peak_values: (f32, f32)) -> Self { + let range = 0.0..=100.0; + let slf = Self::from_get_set( + range, + move |v: Option| { + if let Some(v) = v { + *value = v as f32 / 100.0; + } + *value as f64 * 100.0 + }, + peak_values, + ); + + slf.integer() + } + + pub fn from_get_set( + range: RangeInclusive, + get_set_value: impl 'a + FnMut(Option) -> f64, + peak_values: (f32, f32), + ) -> Self { + Self { + get_set_value: Box::new(get_set_value), + range, + spec: SliderSpec { + logarithmic: false, + smallest_positive: 1e-6, + largest_finite: f64::INFINITY, + }, + clamp_to_range: true, + smart_aim: true, + show_value: true, + orientation: SliderOrientation::Horizontal, + prefix: Default::default(), + suffix: Default::default(), + text: Default::default(), + text_color: None, + min_decimals: 0, + max_decimals: None, + peak_values, + } + } + + /// Control whether or not the slider shows the current value. + /// Default: `true`. + pub fn show_value(mut self, show_value: bool) -> Self { + self.show_value = show_value; + self + } + + /// Show a prefix before the number, e.g. "x: " + pub fn prefix(mut self, prefix: impl ToString) -> Self { + self.prefix = prefix.to_string(); + self + } + + /// Add a suffix to the number, this can be e.g. a unit ("°" or " m") + pub fn suffix(mut self, suffix: impl ToString) -> Self { + self.suffix = suffix.to_string(); + self + } + + /// Show a text next to the slider (e.g. explaining what the slider controls). + pub fn text(mut self, text: impl ToString) -> Self { + self.text = text.to_string(); + self + } + + pub fn text_color(mut self, text_color: Color32) -> Self { + self.text_color = Some(text_color); + self + } + + /// Vertical or horizontal slider? The default is horizontal. + pub fn orientation(mut self, orientation: SliderOrientation) -> Self { + self.orientation = orientation; + self + } + + /// Make this a vertical slider. + pub fn vertical(mut self) -> Self { + self.orientation = SliderOrientation::Vertical; + self + } + + /// Make this a logarithmic slider. + /// This is great for when the slider spans a huge range, + /// e.g. from one to a million. + /// The default is OFF. + pub fn logarithmic(mut self, logarithmic: bool) -> Self { + self.spec.logarithmic = logarithmic; + self + } + + /// For logarithmic sliders that includes zero: + /// what is the smallest positive value you want to be able to select? + /// The default is `1` for integer sliders and `1e-6` for real sliders. + pub fn smallest_positive(mut self, smallest_positive: f64) -> Self { + self.spec.smallest_positive = smallest_positive; + self + } + + /// For logarithmic sliders, the largest positive value we are interested in + /// before the slider switches to `INFINITY`, if that is the higher end. + /// Default: INFINITY. + pub fn largest_finite(mut self, largest_finite: f64) -> Self { + self.spec.largest_finite = largest_finite; + self + } + + /// If set to `true`, all incoming and outgoing values will be clamped to the slider range. + /// Default: `true`. + pub fn clamp_to_range(mut self, clamp_to_range: bool) -> Self { + self.clamp_to_range = clamp_to_range; + self + } + + /// Turn smart aim on/off. Default is ON. + /// There is almost no point in turning this off. + pub fn smart_aim(mut self, smart_aim: bool) -> Self { + self.smart_aim = smart_aim; + self + } + + /// Set a minimum number of decimals to display. + /// Normally you don't need to pick a precision, as the slider will intelligently pick a precision for you. + /// Regardless of precision the slider will use "smart aim" to help the user select nice, round values. + pub fn min_decimals(mut self, min_decimals: usize) -> Self { + self.min_decimals = min_decimals; + self + } + + /// Set a maximum number of decimals to display. + /// Values will also be rounded to this number of decimals. + /// Normally you don't need to pick a precision, as the slider will intelligently pick a precision for you. + /// Regardless of precision the slider will use "smart aim" to help the user select nice, round values. + pub fn max_decimals(mut self, max_decimals: usize) -> Self { + self.max_decimals = Some(max_decimals); + self + } + + /// Set an exact number of decimals to display. + /// Values will also be rounded to this number of decimals. + /// Normally you don't need to pick a precision, as the slider will intelligently pick a precision for you. + /// Regardless of precision the slider will use "smart aim" to help the user select nice, round values. + pub fn fixed_decimals(mut self, num_decimals: usize) -> Self { + self.min_decimals = num_decimals; + self.max_decimals = Some(num_decimals); + self + } + + /// Helper: equivalent to `self.precision(0).smallest_positive(1.0)`. + /// If you use one of the integer constructors (e.g. `Slider::i32`) this is called for you, + /// but if you want to have a slider for picking integer values in an `Slider::f64`, use this. + pub fn integer(self) -> Self { + self.fixed_decimals(0).smallest_positive(1.0) + } + + fn get_value(&mut self) -> f64 { + let value = get(&mut self.get_set_value); + if self.clamp_to_range { + let start = *self.range.start(); + let end = *self.range.end(); + value.clamp(start.min(end), start.max(end)) + } else { + value + } + } + + fn set_value(&mut self, mut value: f64) { + if self.clamp_to_range { + let start = *self.range.start(); + let end = *self.range.end(); + value = value.clamp(start.min(end), start.max(end)); + } + if let Some(max_decimals) = self.max_decimals { + value = emath::round_to_decimals(value, max_decimals); + } + set(&mut self.get_set_value, value); + } + + fn clamp_range(&self) -> RangeInclusive { + if self.clamp_to_range { + self.range() + } else { + f64::NEG_INFINITY..=f64::INFINITY + } + } + + fn range(&self) -> RangeInclusive { + self.range.clone() + } + + /// For instance, `position` is the mouse position and `position_range` is the physical location of the slider on the screen. + fn value_from_position(&self, position: f32, position_range: RangeInclusive) -> f64 { + let normalized = remap_clamp(position, position_range, 0.0..=1.0) as f64; + value_from_normalized(normalized, self.range(), &self.spec) + } + + fn position_from_value(&self, value: f64, position_range: RangeInclusive) -> f32 { + let normalized = normalized_from_value(value, self.range(), &self.spec); + lerp(position_range, normalized as f32) + } +} + +impl<'a> VolumeSlider<'a> { + /// Just the slider, no text + fn allocate_slider_space(&self, ui: &mut Ui, perpendicular: f32) -> Response { + let desired_size = match self.orientation { + SliderOrientation::Horizontal => vec2(ui.spacing().slider_width, perpendicular), + SliderOrientation::Vertical => vec2(perpendicular, ui.spacing().slider_width), + }; + ui.allocate_response(desired_size, Sense::click_and_drag()) + } + + /// Just the slider, no text + fn slider_ui(&mut self, ui: &mut Ui, response: &Response) { + let rect = &response.rect; + let position_range = self.position_range(rect); + + if let Some(pointer_position_2d) = response.interact_pointer_pos() { + let position = self.pointer_position(pointer_position_2d); + let new_value = if self.smart_aim { + let aim_radius = ui.input().aim_radius(); + emath::smart_aim::best_in_range_f64( + self.value_from_position(position - aim_radius, position_range.clone()), + self.value_from_position(position + aim_radius, position_range.clone()), + ) + } else { + self.value_from_position(position, position_range.clone()) + }; + self.set_value(new_value); + } + + let value = self.get_value(); + response.widget_info(|| WidgetInfo::slider(value, &self.text)); + + if response.has_focus() { + let (dec_key, inc_key) = match self.orientation { + SliderOrientation::Horizontal => (Key::ArrowLeft, Key::ArrowRight), + // Note that this is for moving the slider position, + // so up = decrement y coordinate: + SliderOrientation::Vertical => (Key::ArrowUp, Key::ArrowDown), + }; + + let decrement = ui.input().num_presses(dec_key); + let increment = ui.input().num_presses(inc_key); + let kb_step = increment as f32 - decrement as f32; + + if kb_step != 0.0 { + let prev_value = self.get_value(); + let prev_position = self.position_from_value(prev_value, position_range.clone()); + let new_position = prev_position + kb_step; + let new_value = if self.smart_aim { + let aim_radius = ui.input().aim_radius(); + emath::smart_aim::best_in_range_f64( + self.value_from_position(new_position - aim_radius, position_range.clone()), + self.value_from_position(new_position + aim_radius, position_range.clone()), + ) + } else { + self.value_from_position(new_position, position_range.clone()) + }; + self.set_value(new_value); + } + } + + // Paint it: + if ui.is_rect_visible(response.rect) { + let value = self.get_value(); + + let rail_radius = ui.painter().round_to_pixel(self.rail_radius_limit(rect)); + let rail_rect = self.rail_rect(rect, rail_radius); + + let position_1d = self.position_from_value(value, position_range.clone()); + let position_peak_left = + self.position_from_value(self.peak_values.0 as f64 * value, position_range.clone()); + let position_peak_right = + self.position_from_value(self.peak_values.1 as f64 * value, position_range); + + let mut peak_left_rect = self.rail_rect(rect, rail_radius); + let mut peak_right_rect = self.rail_rect(rect, rail_radius); + + match self.orientation { + SliderOrientation::Horizontal => { + peak_left_rect.max.x = position_peak_left; + peak_right_rect.max.x = position_peak_right; + peak_left_rect.max.y -= rail_radius; + peak_right_rect.min.y += rail_radius; + } + SliderOrientation::Vertical => { + peak_left_rect.max.y = position_peak_left; + peak_right_rect.max.y = position_peak_right; + peak_left_rect.max.x -= rail_radius; + peak_right_rect.min.x += rail_radius; + } + } + + let visuals = ui.style().interact(response); + ui.painter().add(epaint::RectShape { + rect: rail_rect, + rounding: ui.visuals().widgets.inactive.rounding, + fill: ui.visuals().widgets.inactive.bg_fill, + // fill: visuals.bg_fill, + // fill: ui.visuals().extreme_bg_color, + stroke: Default::default(), + // stroke: visuals.bg_stroke, + // stroke: ui.visuals().widgets.inactive.bg_stroke, + }); + + if self.peak_values.0 > 1e-6 { + ui.painter().add(epaint::RectShape { + rect: peak_left_rect, + rounding: Rounding::same(0.0), + fill: visuals.text_color(), + stroke: Default::default(), + }); + } + + if self.peak_values.1 > 1e-6 { + ui.painter().add(epaint::RectShape { + rect: peak_right_rect, + rounding: Rounding::same(0.0), + fill: visuals.text_color(), + stroke: Default::default(), + }); + } + + let center = self.marker_center(position_1d, &rail_rect); + + ui.painter().add(epaint::RectShape { + rect: self.handle_rect(center, rect), + fill: visuals.bg_fill, + stroke: visuals.fg_stroke, + rounding: Rounding::same(2.0), + }); + } + } + + fn marker_center(&self, position_1d: f32, rail_rect: &Rect) -> Pos2 { + match self.orientation { + SliderOrientation::Horizontal => pos2(position_1d, rail_rect.center().y), + SliderOrientation::Vertical => pos2(rail_rect.center().x, position_1d), + } + } + + fn pointer_position(&self, pointer_position_2d: Pos2) -> f32 { + match self.orientation { + SliderOrientation::Horizontal => pointer_position_2d.x, + SliderOrientation::Vertical => pointer_position_2d.y, + } + } + + fn position_range(&self, rect: &Rect) -> RangeInclusive { + let handle_radius = 0.0; //self.handle_radius(rect); + match self.orientation { + SliderOrientation::Horizontal => { + (rect.left() + handle_radius)..=(rect.right() - handle_radius) + } + SliderOrientation::Vertical => { + (rect.bottom() - handle_radius)..=(rect.top() + handle_radius) + } + } + } + + fn rail_rect(&self, rect: &Rect, radius: f32) -> Rect { + match self.orientation { + SliderOrientation::Horizontal => Rect::from_min_max( + pos2(rect.left(), rect.center().y - radius), + pos2(rect.right(), rect.center().y + radius), + ), + SliderOrientation::Vertical => Rect::from_min_max( + pos2(rect.center().x - radius, rect.top()), + pos2(rect.center().x + radius, rect.bottom()), + ), + } + } + + fn handle_rect(&self, center: Pos2, rect: &Rect) -> Rect { + let size = match self.orientation { + SliderOrientation::Horizontal => Vec2::new(8.0, rect.height() / 1.25), + SliderOrientation::Vertical => Vec2::new(rect.width() / 1.25, 8.0), + }; + + Rect::from_center_size(center, size) + } + + fn rail_radius_limit(&self, rect: &Rect) -> f32 { + match self.orientation { + SliderOrientation::Horizontal => (rect.height() / 4.0).at_least(2.0), + SliderOrientation::Vertical => (rect.width() / 4.0).at_least(2.0), + } + } + + fn label_ui(&mut self, ui: &mut Ui) { + if !self.text.is_empty() { + let text_color = self.text_color.unwrap_or_else(|| ui.visuals().text_color()); + let text = RichText::new(&self.text).color(text_color); + ui.add(Label::new(text).wrap(false)); + } + } + + fn value_ui(&mut self, ui: &mut Ui, position_range: RangeInclusive) -> Response { + let speed = self.current_gradient(&position_range); + + let mut value = self.get_value(); + let response = ui.add( + DragValue::new(&mut value) + .speed(speed) + .clamp_range(self.clamp_range()) + .min_decimals(self.min_decimals) + .max_decimals_opt(self.max_decimals) + .suffix(self.suffix.clone()) + .prefix(self.prefix.clone()), + ); + if value != self.get_value() { + self.set_value(value); + } + response + } + + fn current_gradient(&mut self, position_range: &RangeInclusive) -> f64 { + let value = self.get_value(); + let value_from_pos = + |position: f32| self.value_from_position(position, position_range.clone()); + let pos_from_value = |value: f64| self.position_from_value(value, position_range.clone()); + let left_value = value_from_pos(pos_from_value(value) - 0.5); + let right_value = value_from_pos(pos_from_value(value) + 0.5); + right_value - left_value + } + + fn add_contents(&mut self, ui: &mut Ui) -> Response { + let thickness = ui + .text_style_height(&TextStyle::Body) + .at_least(ui.spacing().interact_size.y); + let mut response = self.allocate_slider_space(ui, thickness); + self.slider_ui(ui, &response); + + if self.show_value { + let position_range = self.position_range(&response.rect); + let value_response = self.value_ui(ui, position_range); + if value_response.gained_focus() + || value_response.has_focus() + || value_response.lost_focus() + { + // Use the [`DragValue`] id as the id of the whole widget, + // so that the focus events work as expected. + response = value_response.union(response); + } else { + // Use the slider id as the id for the whole widget + response = response.union(value_response); + } + } + + if !self.text.is_empty() { + let text_color = self.text_color.unwrap_or_else(|| ui.visuals().text_color()); + let text = RichText::new(&self.text).color(text_color); + ui.add(Label::new(text).wrap(false)); + } + + response + } +} + +impl<'a> Widget for VolumeSlider<'a> { + fn ui(mut self, ui: &mut Ui) -> Response { + let old_value = self.get_value(); + + let inner_response = match self.orientation { + SliderOrientation::Horizontal => ui.horizontal(|ui| self.add_contents(ui)), + SliderOrientation::Vertical => ui.vertical(|ui| self.add_contents(ui)), + }; + + let mut response = inner_response.inner | inner_response.response; + if self.get_value() != old_value { + response.mark_changed(); + } + response + } +} + +// ---------------------------------------------------------------------------- +// Helpers for converting slider range to/from normalized [0-1] range. +// Always clamps. +// Logarithmic sliders are allowed to include zero and infinity, +// even though mathematically it doesn't make sense. + +use std::f64::INFINITY; + +/// When the user asks for an infinitely large range (e.g. logarithmic from zero), +/// give a scale that this many orders of magnitude in size. +const INF_RANGE_MAGNITUDE: f64 = 10.0; + +fn value_from_normalized(normalized: f64, range: RangeInclusive, spec: &SliderSpec) -> f64 { + let (min, max) = (*range.start(), *range.end()); + + if min.is_nan() || max.is_nan() { + f64::NAN + } else if min == max { + min + } else if min > max { + value_from_normalized(1.0 - normalized, max..=min, spec) + } else if normalized <= 0.0 { + min + } else if normalized >= 1.0 { + max + } else if spec.logarithmic { + if max <= 0.0 { + // non-positive range + -value_from_normalized(normalized, -min..=-max, spec) + } else if 0.0 <= min { + let (min_log, max_log) = range_log10(min, max, spec); + let log = lerp(min_log..=max_log, normalized); + 10.0_f64.powf(log) + } else { + assert!(min < 0.0 && 0.0 < max); + let zero_cutoff = logaritmic_zero_cutoff(min, max); + if normalized < zero_cutoff { + // negative + value_from_normalized( + remap(normalized, 0.0..=zero_cutoff, 0.0..=1.0), + min..=0.0, + spec, + ) + } else { + // positive + value_from_normalized( + remap(normalized, zero_cutoff..=1.0, 0.0..=1.0), + 0.0..=max, + spec, + ) + } + } + } else { + egui::egui_assert!( + min.is_finite() && max.is_finite(), + "You should use a logarithmic range" + ); + lerp(range, normalized.clamp(0.0, 1.0)) + } +} + +fn normalized_from_value(value: f64, range: RangeInclusive, spec: &SliderSpec) -> f64 { + let (min, max) = (*range.start(), *range.end()); + + if min.is_nan() || max.is_nan() { + f64::NAN + } else if min == max { + 0.5 // empty range, show center of slider + } else if min > max { + 1.0 - normalized_from_value(value, max..=min, spec) + } else if value <= min { + 0.0 + } else if value >= max { + 1.0 + } else if spec.logarithmic { + if max <= 0.0 { + // non-positive range + normalized_from_value(-value, -min..=-max, spec) + } else if 0.0 <= min { + let (min_log, max_log) = range_log10(min, max, spec); + let value_log = value.log10(); + remap_clamp(value_log, min_log..=max_log, 0.0..=1.0) + } else { + assert!(min < 0.0 && 0.0 < max); + let zero_cutoff = logaritmic_zero_cutoff(min, max); + if value < 0.0 { + // negative + remap( + normalized_from_value(value, min..=0.0, spec), + 0.0..=1.0, + 0.0..=zero_cutoff, + ) + } else { + // positive side + remap( + normalized_from_value(value, 0.0..=max, spec), + 0.0..=1.0, + zero_cutoff..=1.0, + ) + } + } + } else { + egui::egui_assert!( + min.is_finite() && max.is_finite(), + "You should use a logarithmic range" + ); + remap_clamp(value, range, 0.0..=1.0) + } +} + +fn range_log10(min: f64, max: f64, spec: &SliderSpec) -> (f64, f64) { + assert!(spec.logarithmic); + assert!(min <= max); + + if min == 0.0 && max == INFINITY { + (spec.smallest_positive.log10(), INF_RANGE_MAGNITUDE) + } else if min == 0.0 { + if spec.smallest_positive < max { + (spec.smallest_positive.log10(), max.log10()) + } else { + (max.log10() - INF_RANGE_MAGNITUDE, max.log10()) + } + } else if max == INFINITY { + if min < spec.largest_finite { + (min.log10(), spec.largest_finite.log10()) + } else { + (min.log10(), min.log10() + INF_RANGE_MAGNITUDE) + } + } else { + (min.log10(), max.log10()) + } +} + +/// where to put the zero cutoff for logarithmic sliders +/// that crosses zero ? +fn logaritmic_zero_cutoff(min: f64, max: f64) -> f64 { + assert!(min < 0.0 && 0.0 < max); + + let min_magnitude = if min == -INFINITY { + INF_RANGE_MAGNITUDE + } else { + min.abs().log10().abs() + }; + let max_magnitude = if max == INFINITY { + INF_RANGE_MAGNITUDE + } else { + max.log10().abs() + }; + + let cutoff = min_magnitude / (min_magnitude + max_magnitude); + egui_assert!((0.0..=1.0).contains(&cutoff)); + cutoff +} diff --git a/crates/nodio-core/Cargo.toml b/crates/nodio-core/Cargo.toml new file mode 100644 index 0000000..04a7c8b --- /dev/null +++ b/crates/nodio-core/Cargo.toml @@ -0,0 +1,12 @@ +[package] +name = "nodio-core" +version = "0.1.0" +edition = "2021" + +[dependencies] +serde = { version = "1.0", features = ["derive"] } +serde_json = "1.0" + +[dependencies.uuid] +version = "1.0.0-alpha.1" +features = ["v4", "serde"] \ No newline at end of file diff --git a/crates/nodio-core/src/lib.rs b/crates/nodio-core/src/lib.rs new file mode 100644 index 0000000..412f1a5 --- /dev/null +++ b/crates/nodio-core/src/lib.rs @@ -0,0 +1,78 @@ +#![deny(clippy::all)] +mod result; +pub use result::{Error, Result}; + +use serde::{Deserialize, Serialize}; +pub use uuid::Uuid; + +pub trait Context { + fn add_node(&mut self, node: Node); + fn remove_node(&mut self, node_id: Uuid); + fn nodes(&self) -> &[Node]; + fn nodes_mut(&mut self) -> &mut [Node]; + fn connect_node(&mut self, node_id: Uuid, target_id: Uuid) -> Result<()>; + fn disconnect_node(&mut self, node_id: Uuid, target_id: Uuid); + fn set_volume(&mut self, node_id: Uuid, volume: f32); + fn application_processes(&self) -> Vec; + fn input_devices(&self) -> Vec; + fn output_devices(&self) -> Vec; +} + +#[derive(Debug, Clone, PartialOrd, PartialEq, Serialize, Deserialize)] +pub struct Node { + pub id: Uuid, + pub kind: NodeKind, + pub display_name: String, + pub filename: String, + + pub pos: (f32, f32), + + #[serde(skip)] + pub process_id: Option, + #[serde(skip)] + pub active: bool, + #[serde(skip)] + pub present: bool, + #[serde(skip)] + pub volume: f32, + #[serde(skip)] + pub muted: bool, + #[serde(skip)] + pub peak_values: (f32, f32), +} + +impl Default for Node { + fn default() -> Self { + Self { + id: Uuid::new_v4(), + kind: NodeKind::Application, + display_name: String::new(), + filename: String::new(), + pos: (0.0, 0.0), + process_id: None, + active: false, + present: false, + volume: 1.0, + muted: false, + peak_values: (0.0, 0.0), + } + } +} + +#[derive(Debug, Copy, Clone, Ord, PartialOrd, Eq, PartialEq, Serialize, Deserialize)] +pub enum NodeKind { + Application, + OutputDevice, + InputDevice, +} + +pub struct DeviceInfo { + pub id: Uuid, + pub name: String, +} + +pub struct ProcessInfo { + pub pid: u32, + pub display_name: String, + pub filename: String, +} diff --git a/crates/nodio-core/src/result.rs b/crates/nodio-core/src/result.rs new file mode 100644 index 0000000..8c0ada0 --- /dev/null +++ b/crates/nodio-core/src/result.rs @@ -0,0 +1,20 @@ +use std::fmt::{Display, Formatter}; + +pub type Result = std::result::Result; + +#[derive(Clone)] +pub enum Error { + NoSuchDevice, + CouldNotConnect(String), + Other(String), +} + +impl Display for Error { + fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result { + match self { + Error::NoSuchDevice => write!(f, "No such device"), + Error::CouldNotConnect(reason) => write!(f, "Could not connect: {}", reason), + Error::Other(reason) => write!(f, "{}", reason), + } + } +} diff --git a/crates/nodio-gui-nodes/Cargo.toml b/crates/nodio-gui-nodes/Cargo.toml new file mode 100644 index 0000000..6ce8dc1 --- /dev/null +++ b/crates/nodio-gui-nodes/Cargo.toml @@ -0,0 +1,14 @@ +[package] +name = "nodio-gui-nodes" +version = "0.1.0" +edition = "2021" + +[dependencies] +egui = "0.18.1" +derivative = "2.2.0" +log = "0.4.17" +indexmap = "1.8.1" + +[dependencies.uuid] +version = "1.0.0-alpha.1" +features = ["v4"] \ No newline at end of file diff --git a/crates/nodio-gui-nodes/src/lib.rs b/crates/nodio-gui-nodes/src/lib.rs new file mode 100644 index 0000000..bba90e0 --- /dev/null +++ b/crates/nodio-gui-nodes/src/lib.rs @@ -0,0 +1,1397 @@ +#![deny(clippy::all)] +///! +///! Heavily modified clone of [egui_nodes](https://github.com/haighcam/egui_nodes) +///! +use std::cmp::Ordering; +use std::collections::HashMap; + +use derivative::Derivative; +use egui::{pos2, Pos2, Rect, Sense, Ui, Vec2}; +use indexmap::IndexMap; +use log::debug; +use uuid::Uuid; + +use link::*; +use node::*; +use pin::*; + +pub use { + link::LinkArgs, + node::NodeBuilder, + pin::{AttributeFlags, PinArgs, PinShape}, + style::{ColorStyle, Style, StyleFlags, StyleVar}, +}; + +mod link; +mod node; +mod pin; +mod style; + +/// The Context that tracks the state of the node editor +#[derive(Derivative)] +#[derivative(Default, Debug)] +pub struct Context { + #[derivative(Debug = "ignore")] + io: IO, + #[derivative(Debug = "ignore")] + style: Style, + + node_ids_overlapping_with_mouse: Vec, + occluded_pin_ids: Vec, + + canvas_origin_screen_space: Vec2, + #[derivative(Default(value = "[[0.0; 2].into(); 2].into()"))] + canvas_rect_screen_space: Rect, + + hovered_node_id: Option, + interactive_node_id: Option, + hovered_link_id: Option, + hovered_pin_id: Option, + detached_link_id: Option, + dropped_link_id: Option, + snap_link_id: Option, + + hovered_pin_flags: usize, + ui_element_hovered: bool, + + element_state_change: usize, + + active_attribute_id: Uuid, + active_attribute: bool, + + mouse_pos: Pos2, + mouse_delta: Vec2, + + left_mouse_pressed: bool, + left_mouse_released: bool, + alt_mouse_clicked: bool, + left_mouse_dragging: bool, + alt_mouse_dragging: bool, + mouse_in_canvas: bool, + link_detach_with_modifier_click: bool, + + nodes: IndexMap, + pins: IndexMap, + links: IndexMap, + + end_pin_link_mapping: HashMap>, + + panning: Vec2, + + selected_node_ids: Vec, + selected_link_ids: Vec, + + node_depth_order: Vec, + + partial_link: Option<(Uuid, Option)>, + + #[derivative(Default(value = "ClickInteractionType::None"))] + click_interaction_type: ClickInteractionType, + click_interaction_state: ClickInteractionState, +} + +impl Context { + pub fn begin_frame(&mut self, ui: &mut Ui) { + self.hovered_node_id.take(); + self.interactive_node_id.take(); + self.hovered_link_id.take(); + self.hovered_pin_flags = AttributeFlags::None as usize; + self.detached_link_id.take(); + self.dropped_link_id.take(); + self.snap_link_id.take(); + self.partial_link.take(); + self.end_pin_link_mapping.clear(); + self.node_ids_overlapping_with_mouse.clear(); + self.element_state_change = ElementStateChange::None as usize; + self.active_attribute = false; + self.canvas_rect_screen_space = ui.available_rect_before_wrap(); + self.canvas_origin_screen_space = self.canvas_rect_screen_space.min.to_vec2(); + + for node in self.nodes.values_mut() { + node.in_use = false; + } + for pin in self.pins.values_mut() { + pin.in_use = false; + } + for link in self.links.values_mut() { + link.in_use = false; + } + + ui.set_min_size(self.canvas_rect_screen_space.size()); + + let mut ui = ui.child_ui( + self.canvas_rect_screen_space, + egui::Layout::top_down(egui::Align::Center), + ); + + let screen_rect = ui.ctx().input().screen_rect(); + ui.set_clip_rect(self.canvas_rect_screen_space.intersect(screen_rect)); + + ui.painter().rect_filled( + self.canvas_rect_screen_space, + 0.0, + self.style.colors[ColorStyle::GridBackground as usize], + ); + + if (self.style.flags & StyleFlags::GridLines as usize) != 0 { + self.draw_grid(self.canvas_rect_screen_space.size(), &mut ui); + } + } + + pub fn end_frame(&mut self, ui: &mut Ui) -> egui::Response { + let response = ui.interact( + self.canvas_rect_screen_space, + ui.id().with("Input"), + Sense::click_and_drag(), + ); + + let mouse_pos = if let Some(mouse_pos) = response.hover_pos() { + self.mouse_in_canvas = true; + mouse_pos + } else { + self.mouse_in_canvas = false; + self.mouse_pos + }; + self.mouse_delta = mouse_pos - self.mouse_pos; + self.mouse_pos = mouse_pos; + + let left_mouse_pressed = ui + .ctx() + .input() + .pointer + .button_down(egui::PointerButton::Primary); + self.left_mouse_released = + (self.left_mouse_pressed || self.left_mouse_dragging) && !left_mouse_pressed; + self.left_mouse_dragging = + (self.left_mouse_pressed || self.left_mouse_dragging) && left_mouse_pressed; + self.left_mouse_pressed = + left_mouse_pressed && !(self.left_mouse_pressed || self.left_mouse_dragging); + + let alt_mouse_clicked = self + .io + .emulate_three_button_mouse + .is_active(&ui.ctx().input().modifiers) + || self + .io + .alt_mouse_button + .map_or(false, |x| ui.ctx().input().pointer.button_down(x)); + self.alt_mouse_dragging = + (self.alt_mouse_clicked || self.alt_mouse_dragging) && alt_mouse_clicked; + self.alt_mouse_clicked = + alt_mouse_clicked && !(self.alt_mouse_clicked || self.alt_mouse_dragging); + self.link_detach_with_modifier_click = self + .io + .link_detach_with_modifier_click + .is_active(&ui.ctx().input().modifiers); + + if self.mouse_in_canvas { + self.resolve_occluded_pins(); + self.resolve_hovered_pin(); + + if self.hovered_pin_id.is_none() { + self.resolve_hovered_node(); + } + } + + self.click_interaction_update(ui); + + if self.mouse_in_canvas && self.hovered_node_id.is_none() { + self.resolve_hovered_link(); + } + + for node_id in self.node_depth_order.clone() { + self.draw_node(node_id, ui); + } + + let link_ids = self.links.keys().cloned().collect::>(); + for link_id in link_ids { + self.draw_link(link_id, ui); + } + + if self.left_mouse_pressed || self.alt_mouse_clicked { + self.begin_canvas_interaction(); + } + + self.nodes.retain(|node_id, node| { + if node.in_use { + node.pin_ids.clear(); + true + } else { + self.node_depth_order.retain(|id| id != node_id); + false + } + }); + + self.pins.retain(|_, pin| pin.in_use); + self.links.retain(|_, link| link.in_use); + + ui.painter().rect_stroke( + self.canvas_rect_screen_space, + 0.0, + (1.0, self.style.colors[ColorStyle::GridLine as usize]), + ); + + response + } + + pub fn style_mut(&mut self) -> &mut Style { + &mut self.style + } + + pub fn node_pos(&self, node_id: Uuid) -> Option { + self.nodes.get(&node_id).map(|node| node.origin) + } + + /// Check if there is a node that is hovered by the pointer + pub fn hovered_node(&self) -> Option { + self.hovered_node_id + } + + /// Check if there is a link that is hovered by the pointer + pub fn hovered_link(&self) -> Option { + self.hovered_link_id + } + + /// Check if there is a pin that is hovered by the pointer + pub fn hovered_pin(&self) -> Option { + self.hovered_pin_id + } + + pub fn num_selected_nodes(&self) -> usize { + self.selected_link_ids.len() + } + + pub fn get_selected_nodes(&self) -> &[Uuid] { + &self.selected_node_ids + } + + pub fn get_selected_links(&self) -> &[Uuid] { + &self.selected_link_ids + } + + pub fn clear_node_selection(&mut self) { + self.selected_node_ids.clear() + } + + pub fn clear_link_selection(&mut self) { + self.selected_link_ids.clear() + } + + pub fn active_attribute(&self) -> Option { + if self.active_attribute { + Some(self.active_attribute_id) + } else { + None + } + } + + /// Has a new link been created from a pin? + pub fn started_link_pin(&self) -> Option { + if (self.element_state_change & ElementStateChange::LinkStarted as usize) != 0 { + Some(self.click_interaction_state.link_creation.start_pin_id) + } else { + None + } + } + + /// Has a detached link been dropped? + pub fn dropped_link(&self) -> Option { + self.dropped_link_id + } + + pub fn created_link(&self) -> Option<(Uuid, Uuid, bool)> { + if (self.element_state_change & ElementStateChange::LinkCreated as usize) != 0 { + let mut start_pin_id = self.click_interaction_state.link_creation.start_pin_id; + let mut end_pin_id = self + .click_interaction_state + .link_creation + .end_pin_id + .unwrap(); + + if self.pins.get(&start_pin_id).unwrap().kind != AttributeKind::Output { + std::mem::swap(&mut start_pin_id, &mut end_pin_id); + } + + let created_from_snap = + self.click_interaction_type == ClickInteractionType::LinkCreation; + + Some((start_pin_id, end_pin_id, created_from_snap)) + } else { + None + } + } + + pub fn detached_link(&self) -> Option { + self.detached_link_id + } + + pub fn panning(&self) -> Vec2 { + self.panning + } + + pub fn reset_panning(&mut self, panning: Vec2) { + self.panning = panning; + } + + pub fn node_dimensions(&self, id: Uuid) -> Option { + self.nodes.iter().find_map(|(&node_id, node)| { + if node_id == id { + Some(node.rect.size()) + } else { + None + } + }) + } +} + +impl Context { + pub fn add_node(&mut self, id: Uuid) -> NodeBuilder { + NodeBuilder::new(self, id) + } + + pub(crate) fn show_node<'a>( + &'a mut self, + NodeBuilder { + id: node_id, + header_contents, + attributes, + pos, + .. + }: NodeBuilder<'a>, + ui: &mut Ui, + ) { + let node: &mut Node = self.nodes.entry(node_id).or_insert_with(|| { + let mut node = Node::new(); + if let Some(pos) = pos { + node.origin = pos; + } + debug!( + "New node created at ({}, {}): {}", + node.origin.x, node.origin.y, node_id + ); + + if !self.node_depth_order.contains(&node_id) { + self.node_depth_order.push(node_id); + } + node + }); + node.in_use = true; + + self.style.format_node(node); + node.background_shape + .replace(ui.painter().add(egui::Shape::Noop)); + + let node_origin = node.origin; + let node_size = node.size; + let title_space = node.layout_style.padding.y; + + node.header_shapes.push(ui.painter().add(egui::Shape::Noop)); + node.header_shapes.push(ui.painter().add(egui::Shape::Noop)); + let mut header_content_rect = node.header_content_rect; + + let padding = node.layout_style.padding; + let node_pos = self.grid_space_to_screen_space(node_origin); + + let response = ui.allocate_ui_at_rect(Rect::from_min_size(node_pos, node_size), |ui| { + if let Some(header_contents) = header_contents { + let response = ui.allocate_ui(ui.available_size(), header_contents); + header_content_rect = response.response.rect; + + ui.add_space(title_space); + } + + ui.allocate_space(Vec2::splat(4.0)); + + for NodeAttribute { + id: attr_id, + kind, + pin_args, + add_contents, + } in attributes + { + let response = ui.allocate_ui(ui.available_size(), add_contents); + let response = response.response.union(response.inner); + self.add_attribute(attr_id, node_id, kind, pin_args, response); + } + + ui.rect_contains_pointer(ui.min_rect().expand2(padding)) + }); + + let node: &mut Node = self.nodes.get_mut(&node_id).unwrap(); + + node.rect = response.response.rect.expand2(padding); + node.header_content_rect = header_content_rect.expand2(padding); + + node.header_content_rect.max.x = node.rect.max.x; + + let hovered = response.inner; + if hovered { + self.node_ids_overlapping_with_mouse.push(node_id); + } + } + + fn add_attribute( + &mut self, + pin_id: Uuid, + node_id: Uuid, + kind: AttributeKind, + args: PinArgs, + response: egui::Response, + ) { + if kind != AttributeKind::None { + let pin = self.pins.entry(pin_id).or_default(); + pin.in_use = true; + pin.parent_node_id = node_id; + pin.kind = kind; + pin.attribute_rect = response.rect; + + self.style.format_pin(pin, args); + self.nodes.get_mut(&node_id).unwrap().add_pin(pin_id); + } + + if response.is_pointer_button_down_on() { + self.active_attribute = true; + self.active_attribute_id = pin_id; + self.interactive_node_id.replace(node_id); + } + } + + pub fn add_link( + &mut self, + id: Uuid, + start_pin_id: Uuid, + end_pin_id: Uuid, + args: LinkArgs, + ui: &mut Ui, + ) { + self.end_pin_link_mapping + .entry(end_pin_id) + .or_default() + .push(id); + + let link: &mut LinkData = self.links.entry(id).or_default(); + link.in_use = true; + link.start_pin_id = start_pin_id; + link.end_pin_id = end_pin_id; + + link.shape.replace(ui.painter().add(egui::Shape::Noop)); + self.style.format_link(link, args); + + if (self.click_interaction_type == ClickInteractionType::LinkCreation + && self + .pins + .get(&link.end_pin_id) + .unwrap() + .link_creation_on_snap_enabled() + && self.click_interaction_state.link_creation.start_pin_id == link.start_pin_id + && self.click_interaction_state.link_creation.end_pin_id == Some(link.end_pin_id)) + || (self.click_interaction_state.link_creation.start_pin_id == link.end_pin_id + && self.click_interaction_state.link_creation.end_pin_id == Some(link.start_pin_id)) + { + self.snap_link_id.replace(id); + } + } + + fn draw_grid(&self, canvas_size: Vec2, ui: &mut Ui) { + let mut y = self.panning.y.rem_euclid(self.style.grid_spacing); + while y < canvas_size.y { + let mut x = self.panning.x.rem_euclid(self.style.grid_spacing); + while x < canvas_size.x { + ui.painter().circle_filled( + self.editor_space_to_screen_space([x, y].into()), + 2.0, + self.style.colors[ColorStyle::GridLine as usize], + ); + x += self.style.grid_spacing; + } + + y += self.style.grid_spacing; + } + } + + fn grid_space_to_screen_space(&self, v: Pos2) -> Pos2 { + v + self.canvas_origin_screen_space + self.panning + } + + fn editor_space_to_screen_space(&self, v: Pos2) -> Pos2 { + v + self.canvas_origin_screen_space + } + + fn get_screen_space_pin_coordinates(&self, pin: &PinData) -> Pos2 { + let parent_node_rect = self.nodes.get(&pin.parent_node_id).unwrap().rect; + self.style.get_screen_space_pin_coordinates( + &parent_node_rect, + &pin.attribute_rect, + pin.kind, + ) + } + + fn resolve_occluded_pins(&mut self) { + self.occluded_pin_ids.clear(); + + let depth_stack = &self.node_depth_order; + if depth_stack.len() < 2 { + return; + } + + for depth_idx in 0..(depth_stack.len() - 1) { + let node_below = self.nodes.get(&depth_stack[depth_idx]).unwrap(); + for next_depth in &depth_stack[(depth_idx + 1)..(depth_stack.len())] { + let rect_above = self.nodes.get(next_depth).unwrap().rect; + for pin_id in node_below.pin_ids.iter() { + let pin_pos = self.pins.get(pin_id).unwrap().pos; + if rect_above.contains(pin_pos) { + self.occluded_pin_ids.push(*pin_id); + } + } + } + } + } + + fn resolve_hovered_pin(&mut self) { + let mut smallest_distance = f32::MAX; + self.hovered_pin_id.take(); + + let hover_radius_sqr = self.style.pin_hover_radius.powi(2); + + for (pin_id, pin) in self.pins.iter() { + if self.occluded_pin_ids.contains(pin_id) { + continue; + } + + let distance_sqr = (pin.pos - self.mouse_pos).length_sq(); + if distance_sqr < hover_radius_sqr && distance_sqr < smallest_distance { + smallest_distance = distance_sqr; + self.hovered_pin_id.replace(*pin_id); + } + } + } + + fn resolve_hovered_node(&mut self) { + match self.node_ids_overlapping_with_mouse.len() { + 0 => { + self.hovered_node_id.take(); + } + 1 => { + self.hovered_node_id + .replace(self.node_ids_overlapping_with_mouse[0]); + } + _ => { + let mut largest_depth_idx = -1; + + for node_id in self.node_ids_overlapping_with_mouse.iter() { + for (depth_idx, depth_node_id) in self.node_depth_order.iter().enumerate() { + if *depth_node_id == *node_id && depth_idx as isize > largest_depth_idx { + largest_depth_idx = depth_idx as isize; + self.hovered_node_id.replace(*node_id); + } + } + } + } + } + } + + fn resolve_hovered_link(&mut self) { + let mut smallest_distance = f32::MAX; + self.hovered_link_id.take(); + + let links_clone = self.links.clone(); + for (&link_id, link) in self.links.iter_mut() { + if !self.pins.contains_key(&link.start_pin_id) + || !self.pins.contains_key(&link.end_pin_id) + { + continue; + } + + let start_pin = self.pins.get(&link.start_pin_id).unwrap(); + let end_pin = self.pins.get(&link.end_pin_id).unwrap(); + + let pin_link_count = Self::link_count_for_end_pin( + &self.end_pin_link_mapping, + link.end_pin_id, + &self.partial_link, + ); + let idx = Self::link_index_for_end_pin( + &self.end_pin_link_mapping, + &links_clone, + &self.pins, + &self.partial_link, + link.end_pin_id, + link_id, + start_pin.pos, + ) + .unwrap_or(0); + + let end_pos = if self.hovered_pin_id == Some(link.end_pin_id) && pin_link_count > 1 { + self.style + .calculate_link_end_pos(end_pin.pos, self.mouse_pos, pin_link_count, idx) + } else { + end_pin.pos + }; + + let link_data = LinkBezierData::build( + start_pin.pos, + end_pos, + start_pin.kind, + self.style.link_line_segments_per_length, + ); + + let distance = link_data.get_distance_to_cubic_bezier(&self.mouse_pos); + + if distance < self.style.link_hover_distance && distance < smallest_distance { + smallest_distance = distance; + self.hovered_link_id.replace(link_id); + } + } + } + + fn link_count_for_end_pin( + end_pin_links: &HashMap>, + pin_id: Uuid, + partial_link: &Option<(Uuid, Option)>, + ) -> usize { + let mut count = end_pin_links + .get(&pin_id) + .map(|links| links.len()) + .unwrap_or(0); + + match partial_link { + Some((start_pin_id, Some(partial_link_end_pin_id))) + if *partial_link_end_pin_id == pin_id && *start_pin_id != pin_id => + { + count += 1; + } + _ => {} + } + + count + } + + fn link_index_for_end_pin( + end_pin_links: &HashMap>, + links: &IndexMap, + pins: &IndexMap, + partial_link: &Option<(Uuid, Option)>, + pin_id: Uuid, + link_id: Uuid, + start_pos: Pos2, + ) -> Option { + let end_pin_pos = pins.get(&pin_id)?.pos; + + let link_ids = end_pin_links.get(&pin_id)?; + let mut link_angles = link_ids + .iter() + .filter(|&id| *id != link_id) + .filter_map(|link_id| links.get(link_id).map(|link| (*link_id, link.start_pin_id))) + .filter_map(|(link_id, pin_id)| pins.get(&pin_id).map(|pin| (link_id, pin.pos))) + .chain([(link_id, start_pos)]) + .map(|(link_id, start_pin_pos)| (link_id, (end_pin_pos - start_pin_pos).angle())) + .collect::>(); + + match partial_link { + Some((start_pin_id, Some(end_pin_id))) + if *end_pin_id == pin_id && *start_pin_id != pin_id => + { + link_angles.push(( + Uuid::nil(), + (end_pin_pos - pins.get(start_pin_id).unwrap().pos).angle(), + )); + } + _ => {} + } + + link_angles.sort_by(|(_, angle1), (_, angle2)| { + angle2.partial_cmp(angle1).unwrap_or(Ordering::Equal) + }); + + link_angles.iter().position(|(id, _)| *id == link_id) + } + + fn draw_link(&mut self, link_id: Uuid, ui: &mut Ui) { + let links_clone = self.links.clone(); + let link = self.links.get_mut(&link_id).unwrap(); + + if !link.in_use + || !self.pins.contains_key(&link.start_pin_id) + || !self.pins.contains_key(&link.end_pin_id) + { + return; + } + + let same_pin_link_count = Self::link_count_for_end_pin( + &self.end_pin_link_mapping, + link.end_pin_id, + &self.partial_link, + ); + let idx = Self::link_index_for_end_pin( + &self.end_pin_link_mapping, + &links_clone, + &self.pins, + &self.partial_link, + link.end_pin_id, + link_id, + self.pins.get(&link.start_pin_id).unwrap().pos, + ) + .unwrap_or(0); + + let start_pin = self.pins.get(&link.start_pin_id).unwrap(); + let end_pin = self.pins.get(&link.end_pin_id).unwrap(); + let hovered_pin_id = self.hovered_pin_id; + + let end_pos = if hovered_pin_id == Some(link.end_pin_id) && same_pin_link_count > 1 { + self.style + .calculate_link_end_pos(end_pin.pos, self.mouse_pos, same_pin_link_count, idx) + } else { + end_pin.pos + }; + + let link_bezier_data = LinkBezierData::build( + start_pin.pos, + end_pos, + start_pin.kind, + self.style.link_line_segments_per_length, + ); + let link_shape = link.shape.take().unwrap(); + let link_hovered = self.hovered_link_id == Some(link_id) + && self.click_interaction_type != ClickInteractionType::BoxSelection; + + if link_hovered && self.left_mouse_pressed { + self.begin_link_interaction(link_id); + } + + if self.detached_link_id == Some(link_id) { + return; + } + + let link = self.links.get(&link_id).unwrap(); + let mut link_color = link.color_style.base; + if self.partial_link.is_none() { + if self.selected_link_ids.contains(&link_id) { + link_color = link.color_style.selected; + } else if link_hovered { + link_color = link.color_style.hovered; + } + } + + ui.painter().set( + link_shape, + link_bezier_data.draw((self.style.link_thickness, link_color)), + ); + } + + fn draw_node(&mut self, node_id: Uuid, ui: &mut Ui) { + let node: &mut Node = self.nodes.get_mut(&node_id).unwrap(); + if !node.in_use { + return; + } + + let node_hovered = self.hovered_node_id == Some(node_id) + && self.click_interaction_type != ClickInteractionType::BoxSelection; + + let (node_bg_color, title_bg_color) = if self.selected_node_ids.contains(&node_id) { + ( + node.color_style.background_selected, + node.color_style.header_selected, + ) + } else if node_hovered { + ( + node.color_style.background_hovered, + node.color_style.header_hovered, + ) + } else { + (node.color_style.background, node.color_style.header) + }; + + let painter = ui.painter(); + + if let Some(bg_shape) = node.background_shape.take() { + painter.set( + bg_shape, + egui::Shape::rect_filled( + node.rect, + node.layout_style.corner_rounding, + node_bg_color, + ), + ); + } + + if node.header_content_rect.height() > 0.0 { + if let Some(title_shape) = node.header_shapes.pop() { + painter.set( + title_shape, + egui::Shape::rect_filled( + Rect::from_min_size( + node.header_content_rect.min, + Vec2::new( + node.header_content_rect.width(), + node.layout_style.corner_rounding * 2.0, + ), + ), + node.layout_style.corner_rounding, + title_bg_color, + ), + ); + } + + if let Some(title_shape) = node.header_shapes.pop() { + painter.set( + title_shape, + egui::Shape::rect_filled( + Rect::from_min_size( + node.header_content_rect.min + + Vec2::new(0.0, node.layout_style.corner_rounding), + node.header_content_rect.size() + - Vec2::new(0.0, node.layout_style.corner_rounding), + ), + 0.0, + title_bg_color, + ), + ); + } + } + + for pin_id in node.pin_ids.iter().cloned().collect::>() { + self.draw_pin(pin_id, ui); + } + + if node_hovered && self.left_mouse_pressed && self.interactive_node_id != Some(node_id) { + self.begin_node_selection(node_id); + } + } + + fn draw_pin(&mut self, pin_id: Uuid, ui: &mut Ui) { + let pin: &mut PinData = self.pins.get_mut(&pin_id).unwrap(); + let parent_node_rect = self.nodes.get(&pin.parent_node_id).unwrap().rect; + + pin.pos = self.style.get_screen_space_pin_coordinates( + &parent_node_rect, + &pin.attribute_rect, + pin.kind, + ); + + let mut pin_color = pin.color_style.background; + + let pin_hovered = self.hovered_pin_id == Some(pin_id) + && self.click_interaction_type != ClickInteractionType::BoxSelection; + let pin_shape = pin.shape; + let pin_pos = pin.pos; + + let attached_link_count = + Self::link_count_for_end_pin(&self.end_pin_link_mapping, pin_id, &self.partial_link); + + if pin_hovered { + self.hovered_pin_flags = pin.flags; + pin_color = pin.color_style.hovered; + + if self.left_mouse_pressed && (pin.is_output() || self.hovered_link_id.is_some()) { + self.begin_link_creation(pin_id); + } + } + + if pin_hovered && attached_link_count > 1 { + self.style.draw_hovered_pin( + attached_link_count, + pin_pos, + self.mouse_pos, + pin_shape, + pin_color, + ui, + ); + } else { + self.style.draw_pin( + pin_pos, + pin_shape, + pin_color, + self.style.pin_circle_radius, + ui, + ); + } + } + + fn begin_canvas_interaction(&mut self) { + let any_ui_element_hovered = self.hovered_node_id.is_some() + || self.hovered_link_id.is_some() + || self.hovered_pin_id.is_some(); + + let mouse_not_in_canvas = !self.mouse_in_canvas; + + if self.click_interaction_type != ClickInteractionType::None + || any_ui_element_hovered + || mouse_not_in_canvas + { + return; + } + + if self.alt_mouse_clicked { + self.click_interaction_type = ClickInteractionType::Panning; + } else { + self.click_interaction_type = ClickInteractionType::BoxSelection; + self.click_interaction_state.box_selection.min = self.mouse_pos; + } + } + + fn translate_selected_nodes(&mut self) { + if self.left_mouse_dragging { + let delta = self.mouse_delta; + for node_id in self.selected_node_ids.iter() { + let node = self.nodes.get_mut(node_id).unwrap(); + if node.draggable { + node.origin += delta; + } + } + } + } + + fn should_link_snap_to_pin( + &self, + start_pin: &PinData, + hovered_pin_id: Uuid, + duplicate_link: Option, + ) -> bool { + let end_pin = self.pins.get(&hovered_pin_id).unwrap(); + + if start_pin.parent_node_id == end_pin.parent_node_id { + return false; + } + + if start_pin.kind == end_pin.kind { + return false; + } + + if duplicate_link.map_or(false, |duplicate_id| { + Some(duplicate_id) != self.snap_link_id + }) { + return false; + } + + true + } + + fn box_selector_update_selection(&mut self) -> Rect { + let mut box_rect = self.click_interaction_state.box_selection; + if box_rect.min.x > box_rect.max.x { + std::mem::swap(&mut box_rect.min.x, &mut box_rect.max.x); + } + + if box_rect.min.y > box_rect.max.y { + std::mem::swap(&mut box_rect.min.y, &mut box_rect.max.y); + } + + self.selected_node_ids.clear(); + for (node_id, node) in self.nodes.iter() { + if node.in_use && box_rect.intersects(node.rect) { + self.selected_node_ids.push(*node_id); + } + } + + self.selected_link_ids.clear(); + for (&link_id, link) in self.links.iter().filter(|(_, link)| link.in_use) { + if !self.pins.contains_key(&link.start_pin_id) + || !self.pins.contains_key(&link.end_pin_id) + { + continue; + } + + let pin_start = self.pins.get(&link.start_pin_id).unwrap(); + let pin_end = self.pins.get(&link.end_pin_id).unwrap(); + let node_start_rect = self.nodes.get(&pin_start.parent_node_id).unwrap().rect; + let node_end_rect = self.nodes.get(&pin_end.parent_node_id).unwrap().rect; + let start = self.style.get_screen_space_pin_coordinates( + &node_start_rect, + &pin_start.attribute_rect, + pin_start.kind, + ); + let end = self.style.get_screen_space_pin_coordinates( + &node_end_rect, + &pin_end.attribute_rect, + pin_end.kind, + ); + + if self.rectangle_overlaps_link(&box_rect, &start, &end, pin_start.kind) { + self.selected_link_ids.push(link_id); + } + } + box_rect + } + + #[inline] + fn rectangle_overlaps_link( + &self, + rect: &Rect, + start: &Pos2, + end: &Pos2, + start_type: AttributeKind, + ) -> bool { + let mut lrect = Rect::from_min_max(*start, *end); + if lrect.min.x > lrect.max.x { + std::mem::swap(&mut lrect.min.x, &mut lrect.max.x); + } + + if lrect.min.y > lrect.max.y { + std::mem::swap(&mut lrect.min.y, &mut lrect.max.y); + } + + if rect.intersects(lrect) { + if rect.contains(*start) || rect.contains(*end) { + return true; + } + + let link_data = LinkBezierData::build( + *start, + *end, + start_type, + self.style.link_line_segments_per_length, + ); + return link_data.rectangle_overlaps_bezier(rect); + } + false + } + + fn click_interaction_update(&mut self, ui: &mut Ui) { + match self.click_interaction_type { + ClickInteractionType::BoxSelection => { + self.click_interaction_state.box_selection.max = self.mouse_pos; + let rect = self.box_selector_update_selection(); + + let box_selector_color = self.style.colors[ColorStyle::BoxSelector as usize]; + let box_selector_outline = + self.style.colors[ColorStyle::BoxSelectorOutline as usize]; + ui.painter() + .rect(rect, 0.0, box_selector_color, (1.0, box_selector_outline)); + + if self.left_mouse_released { + let mut ids = Vec::with_capacity(self.selected_node_ids.len()); + let depth_stack = &mut self.node_depth_order; + let selected_nodes = &self.selected_node_ids; + depth_stack.retain(|id| { + if selected_nodes.contains(id) { + ids.push(*id); + false + } else { + true + } + }); + self.node_depth_order.extend(ids); + self.click_interaction_type = ClickInteractionType::None; + } + } + ClickInteractionType::Node => { + self.translate_selected_nodes(); + if self.left_mouse_released { + self.click_interaction_type = ClickInteractionType::None; + } + } + ClickInteractionType::Link => { + if self.left_mouse_released { + self.click_interaction_type = ClickInteractionType::None; + } + } + ClickInteractionType::LinkCreation => { + let maybe_duplicate_link_id = self.hovered_pin_id.and_then(|hovered_pin_id| { + self.find_duplicate_link( + self.click_interaction_state.link_creation.start_pin_id, + hovered_pin_id, + ) + }); + + let should_snap = self.hovered_pin_id.map_or(false, |hovered_pin_id| { + let start_pin = self + .pins + .get(&self.click_interaction_state.link_creation.start_pin_id) + .unwrap(); + self.should_link_snap_to_pin(start_pin, hovered_pin_id, maybe_duplicate_link_id) + }); + + let snapping_pin_changed = self + .click_interaction_state + .link_creation + .end_pin_id + .map_or(false, |pin_id| self.hovered_pin_id != Some(pin_id)); + + if snapping_pin_changed && self.snap_link_id.is_some() { + self.begin_link_detach( + self.snap_link_id.unwrap(), + self.click_interaction_state + .link_creation + .end_pin_id + .unwrap(), + ); + } + + let start_pin = self + .pins + .get(&self.click_interaction_state.link_creation.start_pin_id) + .unwrap(); + let start_pos = self.get_screen_space_pin_coordinates(start_pin); + + self.partial_link = Some(( + self.click_interaction_state.link_creation.start_pin_id, + self.hovered_pin_id, + )); + + let end_pos = if should_snap { + let hovered_pin_id = self.hovered_pin_id.unwrap(); + + let pin_pos = self + .get_screen_space_pin_coordinates(self.pins.get(&hovered_pin_id).unwrap()); + + let same_pin_link_count = Self::link_count_for_end_pin( + &self.end_pin_link_mapping, + hovered_pin_id, + &self.partial_link, + ); + + let idx = Self::link_index_for_end_pin( + &self.end_pin_link_mapping, + &self.links, + &self.pins, + &self.partial_link, + hovered_pin_id, + Uuid::nil(), + start_pos, + ) + .unwrap_or(0); + + if same_pin_link_count > 1 { + self.style.calculate_link_end_pos( + pin_pos, + self.mouse_pos, + same_pin_link_count, + idx, + ) + } else { + pin_pos + } + } else { + self.mouse_pos + }; + + let link_data = LinkBezierData::build( + start_pos, + end_pos, + start_pin.kind, + self.style.link_line_segments_per_length, + ); + ui.painter().add(link_data.draw(( + self.style.link_thickness, + self.style.colors[ColorStyle::Link as usize], + ))); + + let link_creation_on_snap = self.hovered_pin_id.map_or(false, |hovered_pin_id| { + self.pins + .get(&hovered_pin_id) + .unwrap() + .link_creation_on_snap_enabled() + }); + + if !should_snap { + self.click_interaction_state.link_creation.end_pin_id.take(); + } + + let create_link = + should_snap && (self.left_mouse_released || link_creation_on_snap); + + if create_link && maybe_duplicate_link_id.is_none() { + if !self.left_mouse_released + && self.click_interaction_state.link_creation.end_pin_id + == self.hovered_pin_id + { + return; + } + self.element_state_change |= ElementStateChange::LinkCreated as usize; + self.click_interaction_state.link_creation.end_pin_id = self.hovered_pin_id; + } + + if self.left_mouse_released { + self.click_interaction_type = ClickInteractionType::None; + if !create_link { + self.element_state_change |= ElementStateChange::LinkDropped as usize; + if self + .click_interaction_state + .link_creation + .link_creation_type + == LinkCreationType::FromDetach + { + self.dropped_link_id = + self.click_interaction_state.link_creation.detached_link_id; + } + } + } + } + ClickInteractionType::Panning => { + if self.alt_mouse_dragging || self.alt_mouse_clicked { + self.panning += self.mouse_delta; + } else { + self.click_interaction_type = ClickInteractionType::None; + } + } + ClickInteractionType::None => (), + } + } + + fn begin_link_detach(&mut self, id: Uuid, detach_id: Uuid) { + self.click_interaction_state.link_creation.end_pin_id.take(); + + let link = self.links.get(&id).unwrap(); + self.click_interaction_state.link_creation.start_pin_id = if detach_id == link.start_pin_id + { + link.end_pin_id + } else { + link.start_pin_id + }; + self.detached_link_id.replace(id); + } + + fn begin_link_interaction(&mut self, id: Uuid) { + let link = self.links.get(&id).unwrap(); + + if self.click_interaction_type == ClickInteractionType::LinkCreation { + if (self.hovered_pin_flags & AttributeFlags::EnableLinkDetachWithDragClick as usize) + != 0 + { + self.begin_link_detach(id, self.hovered_pin_id.unwrap()); + self.click_interaction_state.link_creation.detached_link_id = Some(id); + self.click_interaction_state + .link_creation + .link_creation_type = LinkCreationType::FromDetach; + } + } else if self.link_detach_with_modifier_click { + let start_pin = self.pins.get(&link.start_pin_id).unwrap(); + let end_pin = self.pins.get(&link.end_pin_id).unwrap(); + let dist_to_start = start_pin.pos.distance(self.mouse_pos); + let dist_to_end = end_pin.pos.distance(self.mouse_pos); + let closest_pin_idx = if dist_to_start < dist_to_end { + link.start_pin_id + } else { + link.end_pin_id + }; + self.click_interaction_type = ClickInteractionType::LinkCreation; + self.begin_link_detach(id, closest_pin_idx); + } else { + self.begin_link_selection(id); + } + } + + fn begin_link_creation(&mut self, hovered_pin_id: Uuid) { + self.click_interaction_type = ClickInteractionType::LinkCreation; + self.click_interaction_state.link_creation.start_pin_id = hovered_pin_id; + self.click_interaction_state.link_creation.end_pin_id.take(); + self.click_interaction_state + .link_creation + .link_creation_type = LinkCreationType::Standard; + self.element_state_change |= ElementStateChange::LinkStarted as usize; + } + + fn begin_link_selection(&mut self, link_id: Uuid) { + self.click_interaction_type = ClickInteractionType::Link; + self.selected_node_ids.clear(); + self.selected_link_ids.clear(); + self.selected_link_ids.push(link_id); + } + + fn find_duplicate_link(&self, start_pin_id: Uuid, end_pin_id: Uuid) -> Option { + self.links.iter().find_map(|(&link_id, link)| { + if link.in_use && link.start_pin_id == start_pin_id && link.end_pin_id == end_pin_id { + Some(link_id) + } else { + None + } + }) + } + + fn begin_node_selection(&mut self, id: Uuid) { + if self.click_interaction_type != ClickInteractionType::None { + return; + } + self.click_interaction_type = ClickInteractionType::Node; + if !self.selected_node_ids.contains(&id) { + self.selected_node_ids.clear(); + self.selected_link_ids.clear(); + self.selected_node_ids.push(id); + + self.node_depth_order.retain(|depth_id| *depth_id != id); + self.node_depth_order.push(id); + } + } +} + +#[derive(Debug)] +enum ElementStateChange { + None = 0, + LinkStarted = 1 << 0, + LinkDropped = 1 << 1, + LinkCreated = 1 << 2, +} + +#[derive(PartialEq, Debug, Copy, Clone)] +enum ClickInteractionType { + Node, + Link, + LinkCreation, + Panning, + BoxSelection, + None, +} + +#[derive(PartialEq, Debug)] +enum LinkCreationType { + Standard, + FromDetach, +} + +#[derive(Derivative, Debug)] +#[derivative(Default)] +struct ClickInteractionStateLinkCreation { + start_pin_id: Uuid, + end_pin_id: Option, + detached_link_id: Option, + #[derivative(Default(value = "LinkCreationType::Standard"))] + link_creation_type: LinkCreationType, +} + +#[derive(Derivative, Debug)] +#[derivative(Default)] +struct ClickInteractionState { + link_creation: ClickInteractionStateLinkCreation, + #[derivative(Default(value = "[[0.0; 2].into(); 2].into()"))] + box_selection: Rect, +} + +/// This controls the modifiers needed for certain mouse interactions +#[derive(Derivative, Debug)] +#[derivative(Default)] +pub struct IO { + /// The Modifier that needs to pressed to pan the editor + #[derivative(Default(value = "Modifiers::None"))] + pub emulate_three_button_mouse: Modifiers, + + // The modifier that needs to be pressed to detach a link instead of creating a new one + #[derivative(Default(value = "Modifiers::None"))] + pub link_detach_with_modifier_click: Modifiers, + + // The mouse button that pans the editor. Should probably not be set to Primary. + #[derivative(Default(value = "Some(egui::PointerButton::Middle)"))] + pub alt_mouse_button: Option, +} + +/// Used to track which Egui Modifier needs to be pressed for certain IO actions +#[derive(Debug)] +pub enum Modifiers { + Alt, + Ctrl, + Shift, + Command, + None, +} + +impl Modifiers { + fn is_active(&self, mods: &egui::Modifiers) -> bool { + match self { + Modifiers::Alt => mods.alt, + Modifiers::Ctrl => mods.ctrl, + Modifiers::Shift => mods.shift, + Modifiers::Command => mods.command, + Modifiers::None => false, + } + } +} diff --git a/crates/nodio-gui-nodes/src/link.rs b/crates/nodio-gui-nodes/src/link.rs new file mode 100644 index 0000000..1845418 --- /dev/null +++ b/crates/nodio-gui-nodes/src/link.rs @@ -0,0 +1,248 @@ +use derivative::Derivative; +use egui::epaint::PathShape; + +use super::*; + +/// The Color Style of a Link. If fields are None then the Context style is used +#[derive(Default, Debug)] +pub struct LinkArgs { + pub base: Option, + pub hovered: Option, + pub selected: Option, +} + +impl LinkArgs { + pub const fn new() -> Self { + Self { + base: None, + hovered: None, + selected: None, + } + } +} + +#[derive(Default, Debug, Copy, Clone)] +pub struct LinkDataColorStyle { + pub base: egui::Color32, + pub hovered: egui::Color32, + pub selected: egui::Color32, +} + +#[derive(Derivative, Copy, Clone)] +#[derivative(Debug)] +pub struct LinkData { + pub in_use: bool, + pub start_pin_id: Uuid, + pub end_pin_id: Uuid, + #[derivative(Debug = "ignore")] + pub color_style: LinkDataColorStyle, + #[derivative(Debug = "ignore")] + pub shape: Option, +} + +impl LinkData { + pub fn new() -> Self { + Self { + in_use: true, + start_pin_id: Uuid::new_v4(), + end_pin_id: Uuid::new_v4(), + color_style: Default::default(), + shape: None, + } + } +} + +impl Default for LinkData { + fn default() -> Self { + Self::new() + } +} + +impl PartialEq for LinkData { + fn eq(&self, rhs: &Self) -> bool { + let mut lhs_start = self.start_pin_id; + let mut lhs_end = self.end_pin_id; + let mut rhs_start = rhs.start_pin_id; + let mut rhs_end = rhs.end_pin_id; + + if lhs_start > lhs_end { + std::mem::swap(&mut lhs_start, &mut lhs_end); + } + + if rhs_start > rhs_end { + std::mem::swap(&mut rhs_start, &mut rhs_end); + } + + lhs_start == rhs_start && lhs_end == rhs_start + } +} + +#[derive(Debug)] +pub struct BezierCurve(Pos2, Pos2, Pos2, Pos2); + +impl BezierCurve { + #[inline] + pub fn eval(&self, t: f32) -> Pos2 { + <[f32; 2]>::from( + (1.0 - t).powi(3) * self.0.to_vec2() + + 3.0 * (1.0 - t).powi(2) * t * self.1.to_vec2() + + 3.0 * (1.0 - t) * t.powi(2) * self.2.to_vec2() + + t.powi(3) * self.3.to_vec2(), + ) + .into() + } + + #[inline] + pub fn _get_containing_rect_for_bezier_curve(&self, hover_distance: f32) -> Rect { + let min = self.0.min(self.3); + let max = self.0.max(self.3); + + let mut rect = Rect::from_min_max(min, max); + rect.extend_with(self.1); + rect.extend_with(self.2); + rect.expand(hover_distance) + } +} + +#[derive(Debug)] +pub(crate) struct LinkBezierData { + pub bezier: BezierCurve, + pub num_segments: usize, +} + +impl LinkBezierData { + #[inline] + pub(crate) fn build( + start: Pos2, + end: Pos2, + start_type: AttributeKind, + line_segments_per_length: f32, + ) -> Self { + let (mut start, mut end) = (start, end); + if start_type == AttributeKind::Input { + std::mem::swap(&mut start, &mut end); + } + + let link_length = end.distance(start); + let offset = egui::vec2(0.25 * link_length, 0.0); + Self { + bezier: BezierCurve(start, start + offset, end - offset, end), + num_segments: 1.max((link_length * line_segments_per_length) as usize), + } + } + + pub(crate) fn get_closest_point_on_cubic_bezier(&self, p: &Pos2) -> Pos2 { + let mut p_last = self.bezier.0; + let mut p_closest = self.bezier.0; + let mut p_closest_dist = f32::MAX; + let t_step = 1.0 / self.num_segments as f32; + for i in 1..=self.num_segments { + let p_current = self.bezier.eval(t_step * i as f32); + let p_line = line_closest_point(&p_last, &p_current, p); + let dist = p.distance_sq(p_line); + if dist < p_closest_dist { + p_closest = p_line; + p_closest_dist = dist; + } + p_last = p_current; + } + p_closest + } + + #[inline] + pub(crate) fn get_distance_to_cubic_bezier(&self, pos: &Pos2) -> f32 { + let point_on_curve = self.get_closest_point_on_cubic_bezier(pos); + pos.distance(point_on_curve) + } + + #[inline] + pub(crate) fn rectangle_overlaps_bezier(&self, rect: &Rect) -> bool { + let mut current = self.bezier.eval(0.0); + let dt = 1.0 / self.num_segments as f32; + for i in 0..self.num_segments { + let next = self.bezier.eval((i + 1) as f32 * dt); + if rectangle_overlaps_line_segment(rect, ¤t, &next) { + return true; + } + current = next; + } + false + } + + pub(crate) fn draw(&self, stroke: impl Into) -> egui::Shape { + let points = std::iter::once(self.bezier.0) + .chain( + (1..self.num_segments) + .map(|x| self.bezier.eval(x as f32 / self.num_segments as f32)), + ) + .chain(std::iter::once(self.bezier.3)) + .collect(); + let path_shape = PathShape { + points, + closed: false, + fill: egui::Color32::TRANSPARENT, + stroke: stroke.into(), + }; + egui::Shape::Path(path_shape) + } +} + +#[inline] +pub fn line_closest_point(a: &Pos2, b: &Pos2, p: &Pos2) -> Pos2 { + let ap = *p - *a; + let ab_dir = *b - *a; + let dot = ap.x * ab_dir.x + ap.y * ab_dir.y; + if dot < 0.0 { + return *a; + } + let ab_len_sqr = ab_dir.x * ab_dir.x + ab_dir.y * ab_dir.y; + if dot > ab_len_sqr { + return *b; + } + *a + ab_dir * dot / ab_len_sqr +} + +#[inline] +fn eval_inplicit_line_eq(p1: &Pos2, p2: &Pos2, p: &Pos2) -> f32 { + (p2.y * p1.y) * p.x + (p1.x * p2.x) * p.y * (p2.x * p1.y - p1.x * p2.y) +} + +#[inline] +fn rectangle_overlaps_line_segment(rect: &Rect, p1: &Pos2, p2: &Pos2) -> bool { + if rect.contains(*p1) || rect.contains(*p2) { + return true; + } + + let mut flip_rect = *rect; + if flip_rect.min.x > flip_rect.max.x { + std::mem::swap(&mut flip_rect.min.x, &mut flip_rect.max.x); + } + + if flip_rect.min.y > flip_rect.max.y { + std::mem::swap(&mut flip_rect.min.y, &mut flip_rect.max.y); + } + + if (p1.x < flip_rect.min.x && p2.x < flip_rect.min.x) + || (p1.x > flip_rect.max.x && p2.x > flip_rect.max.x) + || (p1.y < flip_rect.min.y && p2.y < flip_rect.min.y) + || (p1.y > flip_rect.max.y && p2.y > flip_rect.max.y) + { + return false; + } + + let corner_signs = [ + eval_inplicit_line_eq(p1, p2, &flip_rect.left_bottom()).signum(), + eval_inplicit_line_eq(p1, p2, &flip_rect.left_top()).signum(), + eval_inplicit_line_eq(p1, p2, &flip_rect.right_bottom()).signum(), + eval_inplicit_line_eq(p1, p2, &flip_rect.right_top()).signum(), + ]; + + let mut sum = 0.0; + let mut sum_abs = 0.0; + for sign in corner_signs.iter() { + sum += sign; + sum_abs += sign.abs(); + } + + (sum.abs() - sum_abs).abs() < f32::EPSILON +} diff --git a/crates/nodio-gui-nodes/src/node.rs b/crates/nodio-gui-nodes/src/node.rs new file mode 100644 index 0000000..3cc3d4d --- /dev/null +++ b/crates/nodio-gui-nodes/src/node.rs @@ -0,0 +1,178 @@ +use super::*; +use derivative::Derivative; +use std::collections::HashSet; + +#[derive(Default, Debug)] +pub(crate) struct NodeColorStyle { + pub background: egui::Color32, + pub background_hovered: egui::Color32, + pub background_selected: egui::Color32, + pub header: egui::Color32, + pub header_hovered: egui::Color32, + pub header_selected: egui::Color32, +} + +#[derive(Default, Debug)] +pub struct NodeLayoutStyle { + pub corner_rounding: f32, + pub padding: Vec2, + pub border_thickness: f32, +} + +#[derive(Derivative)] +#[derivative(Debug)] +pub(crate) struct Node { + pub in_use: bool, + pub origin: Pos2, + pub size: Vec2, + pub header_content_rect: Rect, + pub rect: Rect, + #[derivative(Debug = "ignore")] + pub color_style: NodeColorStyle, + pub layout_style: NodeLayoutStyle, + pub pin_ids: HashSet, + pub draggable: bool, + + #[derivative(Debug = "ignore")] + pub header_shapes: Vec, + #[derivative(Debug = "ignore")] + pub background_shape: Option, +} + +impl Node { + pub fn new() -> Self { + Self { + in_use: true, + origin: [100.0; 2].into(), + size: [180.0; 2].into(), + header_content_rect: [[0.0; 2].into(); 2].into(), + rect: [[0.0; 2].into(); 2].into(), + color_style: Default::default(), + layout_style: Default::default(), + pin_ids: Default::default(), + draggable: true, + header_shapes: Vec::new(), + background_shape: None, + } + } + + pub fn add_pin(&mut self, pin_id: Uuid) { + self.pin_ids.insert(pin_id); + } +} + +impl Default for Node { + fn default() -> Self { + Self::new() + } +} + +pub(crate) struct NodeAttribute<'a> { + pub(crate) id: Uuid, + pub(crate) kind: AttributeKind, + pub(crate) pin_args: PinArgs, + pub(crate) add_contents: Box egui::Response + 'a>, +} + +/// Used to construct a node and stores the relevant ui code for its title and attributes +/// This is used so that the nodes can be rendered in the context depth order +#[derive(Derivative)] +#[derivative(Debug)] +pub struct NodeBuilder<'a> { + pub(crate) ctx: Option<&'a mut Context>, + + pub(crate) id: Uuid, + #[derivative(Debug = "ignore")] + pub(crate) header_contents: Option>, + #[derivative(Debug = "ignore")] + pub(crate) attributes: Vec>, + pub(crate) pos: Option, +} + +impl<'a> NodeBuilder<'a> { + /// Create a new node to be displayed in a [Context]. + /// Id should be the same across frames and should not be the same as any + /// other currently used nodes. + pub fn new(ctx: &'a mut Context, id: Uuid) -> Self { + Self { + ctx: Some(ctx), + id, + header_contents: None, + attributes: Vec::new(), + pos: None, + } + } + + /// Add a header with given contents. + pub fn with_header(mut self, add_contents: impl FnOnce(&mut Ui) + 'a) -> Self { + self.header_contents.replace(Box::new(add_contents)); + self + } + + /// Add an input attribute that can be connected to output attributes of other nodes. + /// Id should be the same across frames and should not be the same as any other currently used + /// attributes. + pub fn with_input_attribute( + &mut self, + id: Uuid, + pin_args: PinArgs, + add_contents: impl FnOnce(&mut Ui) -> egui::Response + 'a, + ) -> &mut Self { + self.attributes.push(NodeAttribute { + id, + kind: AttributeKind::Input, + pin_args, + add_contents: Box::new(add_contents), + }); + self + } + + /// Add an output attribute that can be connected to input attributes of other nodes. + /// Id should be the same across frames and should not be the same as any other currently used + /// attributes. + pub fn with_output_attribute( + &mut self, + id: Uuid, + pin_args: PinArgs, + add_contents: impl FnOnce(&mut Ui) -> egui::Response + 'a, + ) -> &mut Self { + self.attributes.push(NodeAttribute { + id, + kind: AttributeKind::Output, + pin_args, + add_contents: Box::new(add_contents), + }); + self + } + + /// Add a static attribute that cannot be connected to other attributes. + /// Id should be the same across frames and should not be the same as any other currently used + /// attributes. + pub fn with_static_attribute( + mut self, + id: Uuid, + add_contents: impl FnOnce(&mut Ui) -> egui::Response + 'a, + ) -> Self { + self.attributes.push(NodeAttribute { + id, + kind: AttributeKind::None, + pin_args: PinArgs::default(), + add_contents: Box::new(add_contents), + }); + self + } + + /// Set the position of the node in screen space when it is first created. + pub fn with_origin(mut self, origin: Pos2) -> Self { + self.pos.replace(origin); + self + } + + pub fn id(&self) -> Uuid { + self.id + } + + pub fn show(mut self, ui: &mut Ui) { + self.ctx.take().unwrap().show_node(self, ui); + } +} diff --git a/crates/nodio-gui-nodes/src/pin.rs b/crates/nodio-gui-nodes/src/pin.rs new file mode 100644 index 0000000..0fee8df --- /dev/null +++ b/crates/nodio-gui-nodes/src/pin.rs @@ -0,0 +1,113 @@ +use super::*; +use derivative::Derivative; + +#[derive(Default, Debug)] +pub struct PinArgs { + pub shape: PinShape, + pub flags: Option, + pub background: Option, + pub hovered: Option, +} + +impl PinArgs { + pub const fn new() -> Self { + Self { + shape: PinShape::CircleFilled, + flags: None, + background: None, + hovered: None, + } + } +} + +#[derive(PartialEq, Clone, Copy, Debug)] +pub(crate) enum AttributeKind { + None, + Input, + Output, +} + +impl Default for AttributeKind { + fn default() -> Self { + Self::None + } +} + +/// Controls the shape of an attribute pin. +#[derive(Clone, Copy, Debug)] +pub enum PinShape { + Circle, + CircleFilled, + Triangle, + TriangleFilled, + Quad, + QuadFilled, +} + +impl Default for PinShape { + fn default() -> Self { + Self::CircleFilled + } +} + +/// Controls the way that attribute pins behave +#[derive(Debug)] +pub enum AttributeFlags { + None = 0, + + /// If there is a link on the node then it will detatch instead of creating a new one. + /// Requires handling of deleted links via Context::link_destroyed + EnableLinkDetachWithDragClick = 1 << 0, + + /// Visual snapping will trigger link creation / destruction + EnableLinkCreationOnSnap = 1 << 1, +} + +#[derive(Default, Debug)] +pub(crate) struct PinDataColorStyle { + pub background: egui::Color32, + pub hovered: egui::Color32, +} + +#[derive(Derivative)] +#[derivative(Debug)] +pub(crate) struct PinData { + pub in_use: bool, + pub parent_node_id: Uuid, + pub attribute_rect: Rect, + pub kind: AttributeKind, + pub shape: PinShape, + pub pos: Pos2, + pub flags: usize, + #[derivative(Debug = "ignore")] + pub color_style: PinDataColorStyle, +} + +impl Default for PinData { + fn default() -> Self { + Self::new() + } +} + +impl PinData { + pub fn new() -> Self { + Self { + in_use: true, + parent_node_id: Default::default(), + attribute_rect: [[0.0; 2].into(); 2].into(), + kind: AttributeKind::None, + shape: PinShape::CircleFilled, + pos: Default::default(), + flags: AttributeFlags::None as usize, + color_style: Default::default(), + } + } + + pub fn is_output(&self) -> bool { + self.kind == AttributeKind::Output + } + + pub fn link_creation_on_snap_enabled(&self) -> bool { + self.flags & AttributeFlags::EnableLinkCreationOnSnap as usize != 0 + } +} diff --git a/crates/nodio-gui-nodes/src/style.rs b/crates/nodio-gui-nodes/src/style.rs new file mode 100644 index 0000000..044f844 --- /dev/null +++ b/crates/nodio-gui-nodes/src/style.rs @@ -0,0 +1,300 @@ +use super::*; + +use egui::{remap, Pos2}; +use std::f32::consts::{FRAC_PI_4, FRAC_PI_8, PI}; + +/// Represents different color style values used by a Context +#[derive(Debug, Clone, Copy)] +pub enum ColorStyle { + NodeBackground = 0, + NodeBackgroundHovered, + NodeBackgroundSelected, + NodeHeader, + NodeHeaderHovered, + NodeHeaderSelected, + Link, + LinkHovered, + LinkSelected, + Pin, + PinHovered, + BoxSelector, + BoxSelectorOutline, + GridBackground, + GridLine, + Count, +} + +/// Represents different style values used by a Context +#[derive(Debug, Clone, Copy)] +pub enum StyleVar { + GridSpacing = 0, + NodeCornerRounding, + NodePaddingHorizontal, + NodePaddingVertical, + NodeBorderThickness, + LinkThickness, + LinkLineSegmentsPerLength, + LinkHoverDistance, + PinCircleRadius, + PinQuadSideLength, + PinTriangleSideLength, + PinLineThickness, + PinHoverRadius, + PinOffset, +} + +/// Controls some style aspects +#[derive(Debug)] +pub enum StyleFlags { + None = 0, + GridLines = 1 << 2, +} + +impl ColorStyle { + /// dark color style + pub fn colors_dark() -> [egui::Color32; ColorStyle::Count as usize] { + let mut colors = [egui::Color32::BLACK; ColorStyle::Count as usize]; + colors[ColorStyle::NodeBackground as usize] = + egui::Color32::from_rgba_unmultiplied(50, 50, 50, 255); + colors[ColorStyle::NodeBackgroundHovered as usize] = + egui::Color32::from_rgba_unmultiplied(75, 75, 75, 255); + colors[ColorStyle::NodeBackgroundSelected as usize] = + egui::Color32::from_rgba_unmultiplied(75, 75, 75, 255); + colors[ColorStyle::NodeHeader as usize] = + egui::Color32::from_rgba_unmultiplied(74, 74, 74, 255); + colors[ColorStyle::NodeHeaderHovered as usize] = + egui::Color32::from_rgba_unmultiplied(94, 94, 94, 255); + colors[ColorStyle::NodeHeaderSelected as usize] = + egui::Color32::from_rgba_unmultiplied(120, 120, 120, 255); + colors[ColorStyle::Link as usize] = + egui::Color32::from_rgba_unmultiplied(60, 133, 224, 255); + colors[ColorStyle::LinkHovered as usize] = + egui::Color32::from_rgba_unmultiplied(60, 150, 250, 255); + colors[ColorStyle::LinkSelected as usize] = + egui::Color32::from_rgba_unmultiplied(60, 150, 250, 255); + colors[ColorStyle::Pin as usize] = egui::Color32::from_rgba_unmultiplied(60, 133, 224, 255); + colors[ColorStyle::PinHovered as usize] = + egui::Color32::from_rgba_unmultiplied(53, 150, 250, 255); + colors[ColorStyle::BoxSelector as usize] = + egui::Color32::from_rgba_unmultiplied(61, 133, 224, 30); + colors[ColorStyle::BoxSelectorOutline as usize] = + egui::Color32::from_rgba_unmultiplied(61, 133, 224, 150); + colors[ColorStyle::GridBackground as usize] = egui::Color32::from_rgb(20, 20, 20); + colors[ColorStyle::GridLine as usize] = egui::Color32::from_rgb(26, 26, 26); + colors + } +} + +#[derive(Debug)] +pub struct Style { + pub grid_spacing: f32, + pub node_corner_rounding: f32, + pub node_padding_horizontal: f32, + pub node_padding_vertical: f32, + pub node_border_thickness: f32, + + pub link_thickness: f32, + pub link_line_segments_per_length: f32, + pub link_hover_distance: f32, + + pub pin_circle_radius: f32, + pub pin_quad_side_length: f32, + pub pin_triangle_side_length: f32, + pub pin_line_thickness: f32, + pub pin_hover_radius: f32, + pub pin_hover_shape_radius: f32, + pub pin_offset: f32, + + pub flags: usize, + pub colors: [egui::Color32; ColorStyle::Count as usize], +} + +impl Default for Style { + fn default() -> Self { + Self { + grid_spacing: 26.0, + node_corner_rounding: 4.0, + node_padding_horizontal: 8.0, + node_padding_vertical: 8.0, + node_border_thickness: 1.0, + link_thickness: 3.0, + link_line_segments_per_length: 0.1, + link_hover_distance: 10.0, + pin_circle_radius: 4.0, + pin_quad_side_length: 7.0, + pin_triangle_side_length: 9.5, + pin_line_thickness: 1.0, + pin_hover_radius: 25.0, + pin_hover_shape_radius: 15.0, + pin_offset: 0.0, + flags: StyleFlags::GridLines as usize, + colors: ColorStyle::colors_dark(), + } + } +} + +impl Style { + pub(crate) fn get_screen_space_pin_coordinates( + &self, + node_rect: &Rect, + attribute_rect: &Rect, + kind: AttributeKind, + ) -> Pos2 { + let x = match kind { + AttributeKind::Input => node_rect.min.x - self.pin_offset, + _ => node_rect.max.x + self.pin_offset, + }; + egui::pos2(x, 0.5 * (attribute_rect.min.y + attribute_rect.max.y)) + } + + pub(crate) fn draw_hovered_pin( + &self, + link_count: usize, + pin_pos: Pos2, + mouse_pos: Pos2, + pin_shape: PinShape, + pin_color: egui::Color32, + ui: &mut Ui, + ) { + ui.painter().add(egui::Shape::circle_stroke( + pin_pos, + self.hovered_pin_radius(pin_pos, mouse_pos), + (self.pin_line_thickness, pin_color), + )); + + self.draw_pin( + pin_pos, + pin_shape, + pin_color, + self.pin_circle_radius / 2.0, + ui, + ); + + for i in 0..link_count { + let pin_pos = self.calculate_link_end_pos(pin_pos, mouse_pos, link_count, i); + self.draw_pin(pin_pos, pin_shape, pin_color, self.pin_circle_radius, ui); + } + } + + pub(crate) fn draw_pin( + &self, + pin_pos: Pos2, + pin_shape: PinShape, + pin_color: egui::Color32, + pin_radius: f32, + ui: &mut Ui, + ) { + let painter = ui.painter(); + + match pin_shape { + PinShape::Circle => painter.add(egui::Shape::circle_stroke( + pin_pos, + pin_radius, + (self.pin_line_thickness, pin_color), + )), + PinShape::CircleFilled => { + painter.add(egui::Shape::circle_filled(pin_pos, pin_radius, pin_color)) + } + PinShape::Quad => painter.add(egui::Shape::rect_stroke( + Rect::from_center_size(pin_pos, [self.pin_quad_side_length / 2.0; 2].into()), + 0.0, + (self.pin_line_thickness, pin_color), + )), + PinShape::QuadFilled => painter.add(egui::Shape::rect_filled( + Rect::from_center_size(pin_pos, [self.pin_quad_side_length / 2.0; 2].into()), + 0.0, + pin_color, + )), + PinShape::Triangle => { + let sqrt_3 = 3f32.sqrt(); + let left_offset = -0.166_666_7 * sqrt_3 * self.pin_triangle_side_length; + let right_offset = 0.333_333_3 * sqrt_3 * self.pin_triangle_side_length; + let verticacl_offset = 0.5 * self.pin_triangle_side_length; + painter.add(egui::Shape::closed_line( + vec![ + pin_pos + (left_offset, verticacl_offset).into(), + pin_pos + (right_offset, 0.0).into(), + pin_pos + (left_offset, -verticacl_offset).into(), + ], + (self.pin_line_thickness, pin_color), + )) + } + PinShape::TriangleFilled => { + let sqrt_3 = 3f32.sqrt(); + let left_offset = -0.166_666_7 * sqrt_3 * self.pin_triangle_side_length; + let right_offset = 0.333_333_3 * sqrt_3 * self.pin_triangle_side_length; + let verticacl_offset = 0.5 * self.pin_triangle_side_length; + painter.add(egui::Shape::convex_polygon( + vec![ + pin_pos + (left_offset, verticacl_offset).into(), + pin_pos + (right_offset, 0.0).into(), + pin_pos + (left_offset, -verticacl_offset).into(), + ], + pin_color, + egui::Stroke::none(), + )) + } + }; + } + + pub(crate) fn hovered_pin_radius(&self, pin_pos: Pos2, mouse_pos: Pos2) -> f32 { + remap( + self.pin_hover_radius - (pin_pos - mouse_pos).length(), + 0.0..=(self.pin_hover_radius - self.pin_hover_shape_radius - 5.0), + 0.0..=self.pin_hover_shape_radius, + ) + .min(self.pin_hover_shape_radius) + } + + pub(crate) fn calculate_link_end_pos( + &self, + pin_pos: Pos2, + mouse_pos: Pos2, + link_count: usize, + link_index: usize, + ) -> Pos2 { + let ang = PI - ((link_count - 1) as f32 * FRAC_PI_8) + (link_index as f32 * FRAC_PI_4); + let pin_radius = self.hovered_pin_radius(pin_pos, mouse_pos); + + pos2( + pin_pos.x + f32::cos(ang) * pin_radius, + pin_pos.y - f32::sin(ang) * pin_radius, + ) + } + + pub(crate) fn format_node(&self, node: &mut Node) { + node.color_style.background = self.colors[ColorStyle::NodeBackground as usize]; + node.color_style.background_hovered = + self.colors[ColorStyle::NodeBackgroundHovered as usize]; + node.color_style.background_selected = + self.colors[ColorStyle::NodeBackgroundSelected as usize]; + node.color_style.header = self.colors[ColorStyle::NodeHeader as usize]; + node.color_style.header_hovered = self.colors[ColorStyle::NodeHeaderHovered as usize]; + node.color_style.header_selected = self.colors[ColorStyle::NodeHeaderSelected as usize]; + node.layout_style.corner_rounding = self.node_corner_rounding; + node.layout_style.padding = + Vec2::new(self.node_padding_horizontal, self.node_padding_vertical); + node.layout_style.border_thickness = self.node_border_thickness; + } + + pub(crate) fn format_pin(&self, pin: &mut PinData, args: PinArgs) { + pin.shape = args.shape; + pin.flags = args.flags.unwrap_or(0); + pin.color_style.background = args + .background + .unwrap_or(self.colors[ColorStyle::Pin as usize]); + pin.color_style.hovered = args + .hovered + .unwrap_or(self.colors[ColorStyle::PinHovered as usize]); + } + + pub(crate) fn format_link(&self, link: &mut LinkData, args: LinkArgs) { + link.color_style.base = args.base.unwrap_or(self.colors[ColorStyle::Link as usize]); + link.color_style.hovered = args + .hovered + .unwrap_or(self.colors[ColorStyle::LinkHovered as usize]); + link.color_style.selected = args + .selected + .unwrap_or(self.colors[ColorStyle::LinkSelected as usize]); + } +} diff --git a/crates/nodio-win32/Cargo.toml b/crates/nodio-win32/Cargo.toml new file mode 100644 index 0000000..550383b --- /dev/null +++ b/crates/nodio-win32/Cargo.toml @@ -0,0 +1,39 @@ +[package] +name = "nodio-win32" +version = "0.1.0" +edition = "2021" + +[dependencies] +nodio-core = { path = "../nodio-core" } + +widestring = "1.0.0-beta.1" +log = "0.4.17" +notify-thread = { git = "https://github.com/urholaukkarinen/notify-thread.git" } +pollster = "0.2.5" +parking_lot = { version = "0.12.0", features = ["deadlock_detection"] } + +[dependencies.windows] +version = "0.37.0" +features = [ + "alloc", + "interface", + "implement", + "Win32_Foundation", + "Win32_Security", + "Win32_Media_Audio", + "Win32_Media_Multimedia", + "Win32_Media_Audio_Endpoints", + "Win32_Media_KernelStreaming", + "Win32_Media_MediaFoundation", + "Win32_System_WinRT", + "Win32_System_Threading", + "Win32_System_ProcessStatus", + "Win32_System_Com", + "Win32_System_Com_StructuredStorage", + "Win32_System_Ole", + "Win32_System_Threading", + "Win32_System_Registry", + "Win32_UI_Shell", + "Win32_UI_Shell_PropertiesSystem", + "Win32_Devices_FunctionDiscovery", +] \ No newline at end of file diff --git a/crates/nodio-win32/src/com.rs b/crates/nodio-win32/src/com.rs new file mode 100644 index 0000000..167faa4 --- /dev/null +++ b/crates/nodio-win32/src/com.rs @@ -0,0 +1,43 @@ +use std::marker::PhantomData; + +use std::ptr::null; +use windows::core::Result; +use windows::Win32::Foundation::RPC_E_CHANGED_MODE; +use windows::Win32::System::Com::{CoInitializeEx, CoUninitialize, COINIT_APARTMENTTHREADED}; + +thread_local!(static COM_INITIALIZED: ComInitialized = { + unsafe { + let result = CoInitializeEx(null(), COINIT_APARTMENTTHREADED); + + + match result { + Err(err) if err.code() != RPC_E_CHANGED_MODE => { + panic!("Failed to initialize COM: {}", err.message()) + + }, + _ => ComInitialized { + result, + _ptr: PhantomData, + } + } + } +}); + +struct ComInitialized { + result: Result<()>, + _ptr: PhantomData<*mut ()>, +} + +impl Drop for ComInitialized { + #[inline] + fn drop(&mut self) { + if self.result.is_ok() { + unsafe { CoUninitialize() } + } + } +} + +#[inline] +pub fn ensure_com_initialized() { + COM_INITIALIZED.with(|_| {}); +} diff --git a/crates/nodio-win32/src/context.rs b/crates/nodio-win32/src/context.rs new file mode 100644 index 0000000..1847fff --- /dev/null +++ b/crates/nodio-win32/src/context.rs @@ -0,0 +1,649 @@ +use std::collections::HashSet; +use std::str::FromStr; +use std::sync::Arc; +use std::thread; +use std::time::Duration; + +use log::{debug, error, info, trace, warn}; +use notify_thread::JoinHandle; +use parking_lot::RwLock; +use windows::core::HSTRING; +use windows::Win32::Media::Audio::{ + eCapture, eConsole, eMultimedia, eRender, EDataFlow, DEVICE_STATEMASK_ALL, +}; +use windows::Win32::System::Threading::GetCurrentProcessId; + +use nodio_core::{Context, DeviceInfo, Node, NodeKind, ProcessInfo, Uuid}; +use nodio_core::{Error, Result}; + +use crate::com::ensure_com_initialized; +use crate::custom::{ + create_audio_policy_config, AudioPolicyConfig, AudioSessionEvent, SessionState, +}; +use crate::device::{ + AudioDevice, DEVINTERFACE_AUDIO_CAPTURE, DEVINTERFACE_AUDIO_RENDER, MMDEVAPI_TOKEN, +}; +use crate::enumerator::AudioDeviceEnumerator; +use crate::loopback::LoopbackSession; +use crate::node::{NodeConnectionInfo, NodeConnectionKind}; +use crate::session::{session_node_match, AudioSession, AudioSessionKind}; + +pub struct Win32Context { + device_enumerator: AudioDeviceEnumerator, + audio_policy_config: Box, + + nodes: Vec, + + node_connections: Vec, + + loopback_sessions: Arc>>, + + sessions: Arc>>, + input_devices: Arc>>, + output_devices: Arc>>, + + session_update_thread: Option>, +} + +unsafe impl Send for Win32Context {} +unsafe impl Sync for Win32Context {} + +impl Drop for Win32Context { + fn drop(&mut self) { + self.session_update_thread.take().and_then(|thread| { + thread.notify(); + thread.join().ok() + }); + } +} + +impl Win32Context { + pub fn new() -> Arc> { + ensure_com_initialized(); + + let device_enumerator = AudioDeviceEnumerator::create().unwrap(); + + let ctx = Arc::new(RwLock::new(Win32Context { + device_enumerator, + audio_policy_config: create_audio_policy_config(), + nodes: vec![], + sessions: Default::default(), + input_devices: Default::default(), + output_devices: Default::default(), + node_connections: Default::default(), + loopback_sessions: Default::default(), + session_update_thread: None, + })); + + let mut output_devices = ctx + .read() + .device_enumerator + .enumerate_audio_endpoints(eRender, DEVICE_STATEMASK_ALL) + .unwrap(); + + let mut input_devices = ctx + .read() + .device_enumerator + .enumerate_audio_endpoints(eCapture, DEVICE_STATEMASK_ALL) + .unwrap(); + + for device in input_devices.iter_mut().chain(output_devices.iter_mut()) { + let ctx = ctx.clone(); + let name = device.name().to_string(); + + device.set_session_notification_callback(move |event| { + trace!("Session notification in {}: {:?}", name, event); + + Self::refresh_sessions(ctx.clone()); + }); + } + + ctx.write().input_devices = Arc::new(RwLock::new(input_devices)); + ctx.write().output_devices = Arc::new(RwLock::new(output_devices)); + + Self::refresh_sessions(ctx.clone()); + + let session_update_thread = { + let ctx = ctx.clone(); + + notify_thread::spawn(move |thread| { + trace!("Session update thread started"); + + while !thread.notified() { + let sessions: Arc<_> = ctx.read().sessions.clone(); + let input_devices: Arc<_> = ctx.read().input_devices.clone(); + let output_devices: Arc<_> = ctx.read().output_devices.clone(); + + for session in sessions.read().iter() { + if let Some(node) = ctx + .write() + .nodes + .iter_mut() + .find(|n| session_node_match(n, session)) + { + node.process_id = Some(session.process_id()); + node.peak_values = session.peak_values().unwrap_or((0.0, 0.0)); + node.volume = session.master_volume(); + node.active = session.is_active(); + node.present = true; + } + } + + for device in input_devices + .read() + .iter() + .chain(output_devices.read().iter()) + { + if let Some(node) = + ctx.write().nodes.iter_mut().find(|n| n.id == device.id()) + { + node.peak_values = device.peak_values().unwrap_or((0.0, 0.0)); + node.volume = device.master_volume(); + node.active = device.is_active(); + node.present = true; + } + } + + thread::sleep(Duration::from_secs_f32(1.0 / 30.0)); + } + + trace!("Session update thread stopped"); + }) + }; + + ctx.write().session_update_thread = Some(session_update_thread); + + ctx + } + + fn refresh_sessions(ctx: Arc>) { + debug!("Refreshing sessions"); + + let mut sessions = Vec::new(); + + for device in ctx + .read() + .output_devices + .read() + .iter() + .filter(|d| d.is_active()) + { + let device_sessions = match device.enumerate_sessions() { + Ok(sessions) => sessions, + Err(err) => { + error!("Failed to get device sessions: {}", err); + continue; + } + }; + + for mut session in device_sessions { + let pid = session.process_id(); + if pid == 0 { + continue; + } + + session.set_event_callback({ + let ctx = ctx.clone(); + let session = session.clone(); + + move |event| { + trace!("Session event: {:?}", event); + match event { + AudioSessionEvent::VolumeChange { level, muted } => { + if let Some(node) = ctx + .write() + .nodes + .iter_mut() + .find(|n| session_node_match(n, &session)) + { + node.volume = level; + node.muted = muted; + } + } + AudioSessionEvent::StateChange(state) => { + if let Some(node) = ctx + .write() + .nodes + .iter_mut() + .find(|n| session_node_match(n, &session)) + { + node.peak_values = (0.0, 0.0); + node.active = state == SessionState::Active; + node.present = state != SessionState::Expired; + } + } + AudioSessionEvent::Disconnect(reason) => { + trace!("Session disconnected. Reason: {:?}", reason); + + ctx.write() + .sessions + .write() + .retain(|s| s.id() != session.id()); + } + } + } + }); + + sessions.push(session); + } + } + + ctx.write().sessions = Arc::new(RwLock::new(sessions)); + } + + fn parse_mmdevice_id(mmdevice_id: &str) -> Option<(Uuid, EDataFlow)> { + mmdevice_id + .split(MMDEVAPI_TOKEN) + .nth(1) + .and_then(|s| s.split_once('#')) + .and_then(|(device_id, data_flow)| { + let device_id = device_id.to_string(); + let device_id = Uuid::from_str(&device_id[1..device_id.len() - 1]).unwrap(); + + match data_flow { + DEVINTERFACE_AUDIO_RENDER => Some((device_id, eRender)), + DEVINTERFACE_AUDIO_CAPTURE => Some((device_id, eCapture)), + _ => None, + } + }) + } + + fn get_default_audio_endpoint_for_process( + &self, + process_id: u32, + ) -> windows::core::Result<(Uuid, EDataFlow)> { + let device_id = unsafe { + self.audio_policy_config.persistent_default_audio_endpoint( + process_id, + eRender, + eMultimedia, + )? + }; + + Ok(Self::parse_mmdevice_id(&device_id.to_string()).unwrap_or((Uuid::nil(), eRender))) + } + + fn use_system_default_audio_endpoint_for_process( + &self, + process_id: u32, + ) -> windows::core::Result<()> { + self.set_default_audio_endpoint_for_process(process_id, HSTRING::new()) + } + + fn set_default_audio_endpoint_for_process( + &self, + process_id: u32, + device_id: HSTRING, + ) -> windows::core::Result<()> { + unsafe { + self.audio_policy_config + .set_persistent_default_audio_endpoint( + process_id, + eRender, + eMultimedia, + device_id.clone(), + )?; + + self.audio_policy_config + .set_persistent_default_audio_endpoint(process_id, eRender, eConsole, device_id) + } + } + + fn connect_application_node(&mut self, node_id: Uuid, target_id: Uuid) -> Result<()> { + let node = self.nodes.iter().find(|n| n.id == node_id).unwrap(); + + if node.process_id.is_none() { + return Err(Error::CouldNotConnect("No such process".to_string())); + } + + let output_devices = self.output_devices.read(); + let target_device = output_devices.iter().find(|d| d.id() == target_id).unwrap(); + + let mut conn_info = NodeConnectionInfo { + id: Uuid::new_v4(), + src_id: node_id, + dst_id: target_id, + kind: NodeConnectionKind::DefaultEndpoint, + }; + + if self + .node_connections + .iter() + .any(|conn| conn.src_id == node_id) + { + info!("Already connected, using loopback for stream duplication"); + + let loopback_session = LoopbackSession::start( + node_id, + target_id, + node.process_id.unwrap(), + target_device.mmdevice(), + ) + .map_err(|err| { + error!("Could not start loopback session: {}", err); + Error::CouldNotConnect(err.to_string()) + })?; + + conn_info.kind = NodeConnectionKind::Loopback; + + self.loopback_sessions.write().push(loopback_session); + } else if let Some(session) = self + .sessions + .read() + .iter() + .find(|session| session_node_match(node, session)) + { + match self.get_default_audio_endpoint_for_process(session.process_id()) { + Ok((device_id, _)) => { + if device_id != target_device.id() { + if let Err(err) = self.set_default_audio_endpoint_for_process( + session.process_id(), + target_device.mmdevice_id(eRender), + ) { + error!( + "Failed to set audio endpoint for process {}: {:?}", + session.process_id(), + err + ); + return Err(Error::CouldNotConnect(err.to_string())); + } else { + debug!( + "Set default audio endpoint for process {}", + session.process_id() + ); + } + } else { + debug!("Endpoint is already the same"); + } + } + Err(err) => { + error!( + "Failed to get default endpoint for process {}: {}", + session.process_id(), + err + ); + } + } + } + + self.node_connections.push(conn_info); + + Ok(()) + } + + fn connect_input_device(&mut self, node_id: Uuid, target_id: Uuid) -> Result<()> { + let input_devices = self.input_devices.write(); + let output_devices = self.output_devices.read(); + + let input_device = input_devices + .iter() + .find(|device| device.id() == node_id) + .ok_or_else(|| Error::CouldNotConnect("no such input device found".to_string()))?; + + let output_device = output_devices + .iter() + .find(|device| device.id() == target_id) + .ok_or_else(|| Error::CouldNotConnect("no such output device found".to_string()))?; + + if let Err(err) = input_device.set_listen(Some(output_device)) { + warn!( + "Failed to enable listening on device {}: {}", + input_device.name(), + err + ); + return Err(Error::CouldNotConnect(err.to_string())); + } + + self.node_connections.push(NodeConnectionInfo { + id: Uuid::new_v4(), + src_id: node_id, + dst_id: target_id, + kind: NodeConnectionKind::Listen, + }); + + Ok(()) + } + + fn output_device_exists(&self, id: Uuid) -> bool { + self.output_devices.read().iter().any(|d| d.id() == id) + } +} + +impl Context for Win32Context { + fn add_node(&mut self, mut node: Node) { + if self.nodes.iter().any(|other| other.id == node.id) { + info!("Node already added: {}", &node.display_name); + return; + } + + if let Some(session) = self + .sessions + .read() + .iter() + .find(|&session| session_node_match(&node, session)) + { + node.process_id = Some(session.process_id()); + } + + self.nodes.push(node); + } + + fn remove_node(&mut self, node_id: Uuid) { + let connections = self + .node_connections + .iter() + .filter(|conn| conn.src_id == node_id || conn.dst_id == node_id) + .copied() + .collect::>(); + + for conn in connections { + self.disconnect_node(conn.src_id, conn.dst_id); + } + + self.nodes.retain(|node| node.id != node_id); + } + + fn nodes(&self) -> &[Node] { + self.nodes.as_slice() + } + + fn nodes_mut(&mut self) -> &mut [Node] { + &mut self.nodes + } + + fn connect_node(&mut self, node_id: Uuid, target_id: Uuid) -> Result<()> { + let node_kind = match self.nodes.iter().find(|n| n.id == node_id) { + Some(node) => node.kind, + None => { + warn!("No node found for id {}", node_id); + return Err(Error::CouldNotConnect("No such node found".to_string())); + } + }; + + if !self.output_device_exists(target_id) { + warn!("No output device found for node id: {}", target_id); + return Err(Error::NoSuchDevice); + } + + match node_kind { + NodeKind::Application => self.connect_application_node(node_id, target_id)?, + NodeKind::InputDevice => self.connect_input_device(node_id, target_id)?, + + NodeKind::OutputDevice => { + warn!("Output device cannot be used as an input!"); + return Err(Error::CouldNotConnect( + "Output device cannot be used as an input!".to_string(), + )); + } + } + + Ok(()) + } + + fn disconnect_node(&mut self, src_id: Uuid, dst_id: Uuid) { + let removed_connection = match self + .node_connections + .iter() + .position(|conn| conn.src_id == src_id && conn.dst_id == dst_id) + .map(|idx| self.node_connections.remove(idx)) + { + Some(conn) => conn, + None => { + warn!("No such connection found"); + return; + } + }; + + info!("Removed connection {} => {}", src_id, dst_id); + + let node = match self.nodes.iter().find(|node| node.id == src_id) { + Some(node) => node, + None => { + warn!("No such node found"); + return; + } + }; + + match node.kind { + NodeKind::Application => { + if node.process_id.is_none() { + return; + } + + match removed_connection.kind { + NodeConnectionKind::DefaultEndpoint => { + let next_src_connection = self + .node_connections + .iter_mut() + .find(|conn| conn.src_id == src_id); + + if let Some(next_conn) = next_src_connection { + if next_conn.kind == NodeConnectionKind::Loopback { + self.loopback_sessions.write().retain(|s| { + s.src_id != next_conn.src_id || s.dst_id != next_conn.dst_id + }); + } + + next_conn.kind = NodeConnectionKind::DefaultEndpoint; + + let target_mmdevice_id = self + .output_devices + .read() + .iter() + .find(|d| d.id() == next_conn.dst_id) + .map(|d| d.mmdevice_id(eRender)) + .unwrap(); + + self.set_default_audio_endpoint_for_process( + node.process_id.unwrap(), + target_mmdevice_id, + ) + .ok(); + } else { + self.use_system_default_audio_endpoint_for_process( + node.process_id.unwrap(), + ) + .ok(); + } + } + NodeConnectionKind::Loopback => { + self.loopback_sessions + .write() + .retain(|s| s.src_id != src_id || s.dst_id != dst_id); + } + _ => {} + } + } + + NodeKind::InputDevice => { + if let Some(device) = self + .input_devices + .write() + .iter_mut() + .find(|device| device.id() == src_id) + { + if let Err(err) = device.set_listen(None) { + warn!( + "Failed to enable listening on device {}: {}", + &device.name(), + err + ) + } + } else { + warn!("No input device found for id {}", src_id); + } + } + _ => {} + } + } + + fn set_volume(&mut self, node_id: Uuid, volume: f32) { + if let Some(node) = self.nodes.iter().find(|n| n.id == node_id) { + for matching_session in self + .sessions + .read() + .iter() + .filter(|session| session_node_match(node, session)) + { + matching_session.set_master_volume(volume); + } + + for matching_device in self + .output_devices + .read() + .iter() + .filter(|device| device.id() == node_id) + { + matching_device.set_master_volume(volume); + } + } + } + + fn application_processes(&self) -> Vec { + let mut added_pids = HashSet::new(); + let mut processes = Vec::new(); + + let my_pid = unsafe { GetCurrentProcessId() }; + + for session in self + .sessions + .read() + .iter() + .filter(|s| s.kind() == AudioSessionKind::Application && s.process_id() != my_pid) + { + if added_pids.insert(session.process_id()) { + processes.push(ProcessInfo { + pid: session.process_id(), + display_name: session.display_name().to_string(), + filename: session.filename().to_string(), + }); + } + } + + processes + } + + fn input_devices(&self) -> Vec { + self.input_devices + .read() + .iter() + .filter(|d| !d.id().is_nil() && d.is_active()) + .map(|d| DeviceInfo { + id: d.id(), + name: d.name().to_string(), + }) + .collect::>() + } + + fn output_devices(&self) -> Vec { + self.output_devices + .read() + .iter() + .filter(|d| !d.id().is_nil() && d.is_active()) + .map(|d| DeviceInfo { + id: d.id(), + name: d.name().to_string(), + }) + .collect::>() + } +} diff --git a/crates/nodio-win32/src/custom.rs b/crates/nodio-win32/src/custom.rs new file mode 100644 index 0000000..f4dd314 --- /dev/null +++ b/crates/nodio-win32/src/custom.rs @@ -0,0 +1,950 @@ +#![allow(non_snake_case)] + +use std::ffi::c_void; +use std::fmt::Debug; +use std::mem; +use std::mem::{ManuallyDrop, MaybeUninit}; +use std::ptr::null_mut; +use std::sync::mpsc::Sender; + +use log::warn; +use widestring::U16CStr; +use windows::core::{IUnknown, IUnknownVtbl, PCWSTR}; +use windows::Win32::Foundation::{BOOL, E_NOINTERFACE, S_OK}; +use windows::Win32::Media::Audio::{ + eCapture, eCommunications, eConsole, eMultimedia, eRender, AudioSessionDisconnectReason, + AudioSessionState, AudioSessionStateActive, AudioSessionStateExpired, + AudioSessionStateInactive, DisconnectReasonDeviceRemoval, + DisconnectReasonExclusiveModeOverride, DisconnectReasonFormatChanged, + DisconnectReasonServerShutdown, DisconnectReasonSessionDisconnected, + DisconnectReasonSessionLogoff, IAudioSessionControl, IAudioSessionControl2, + IAudioSessionEvents, IAudioSessionEvents_Vtbl, IAudioSessionNotification, + IAudioSessionNotification_Vtbl, IMMNotificationClient, IMMNotificationClient_Vtbl, + AUDIO_VOLUME_NOTIFICATION_DATA, DEVICE_STATE_ACTIVE, DEVICE_STATE_DISABLED, + DEVICE_STATE_NOTPRESENT, DEVICE_STATE_UNPLUGGED, +}; +use windows::Win32::System::Registry::{ + GetRegistryValueWithFallbackW, HKEY_LOCAL_MACHINE, RRF_RT_REG_SZ, +}; +use windows::Win32::System::WinRT::RoGetActivationFactory; +use windows::Win32::UI::Shell::PropertiesSystem::PROPERTYKEY; +use windows::{ + core::{ + IInspectable, IInspectableVtbl, Interface, IntoParam, Param, RawPtr, Result, GUID, HRESULT, + HSTRING, + }, + Win32::Media::Audio::{EDataFlow, ERole}, +}; + +fn os_version() -> u32 { + let mut os_version: [u16; 512] = [0; 512]; + + let status = unsafe { + GetRegistryValueWithFallbackW( + HKEY_LOCAL_MACHINE, + "SOFTWARE\\Microsoft\\Windows NT\\CurrentVersion", + HKEY_LOCAL_MACHINE, + "SOFTWARE\\Microsoft\\Windows NT\\CurrentVersion", + "CurrentBuild", + RRF_RT_REG_SZ.0, + null_mut(), + &mut os_version as *mut _ as _, + 512, + null_mut(), + ) + } + .0; + + if status != 0 { + panic!("Failed to get os version: WIN32_ERROR({})", status); + } + + HSTRING::from_wide( + &os_version + .iter() + .take_while(|&&c| c != 0) + .copied() + .collect::>(), + ) + .to_string_lossy() + .parse::() + .expect("Failed to parse os version") +} + +pub fn create_audio_policy_config() -> Box { + const LATEST_GUID: u128 = 0xab3d4648_e242_459f_b02f_541c70306324; + const LEGACY_GUID: u128 = 0x2a59116d_6c4f_45e0_a74f_707e3fef9258; + + let name = HSTRING::from("Windows.Media.Internal.AudioPolicyConfig"); + + unsafe { + if os_version() >= 21390 { + Box::new(RoGetActivationFactory::<_, IAudioPolicyConfig>(name).unwrap()) + } else { + Box::new(RoGetActivationFactory::<_, IAudioPolicyConfig>(name).unwrap()) + } + } +} + +pub trait AudioPolicyConfig { + unsafe fn persistent_default_audio_endpoint( + &self, + process_id: u32, + data_flow: EDataFlow, + role: ERole, + ) -> Result; + + unsafe fn set_persistent_default_audio_endpoint( + &self, + process_id: u32, + data_flow: EDataFlow, + role: ERole, + device_id: HSTRING, + ) -> Result<()>; + + unsafe fn clear_all_persisted_default_endpoints(&self) -> Result<()>; +} + +impl AudioPolicyConfig for IAudioPolicyConfig { + unsafe fn persistent_default_audio_endpoint( + &self, + process_id: u32, + data_flow: EDataFlow, + role: ERole, + ) -> Result { + self.GetPersistedDefaultAudioEndpoint(process_id, data_flow, role) + } + + unsafe fn set_persistent_default_audio_endpoint( + &self, + process_id: u32, + data_flow: EDataFlow, + role: ERole, + device_id: HSTRING, + ) -> Result<()> { + self.SetPersistedDefaultAudioEndpoint(process_id, data_flow, role, device_id) + } + + unsafe fn clear_all_persisted_default_endpoints(&self) -> Result<()> { + self.ClearAllPersistedApplicationDefaultEndpoints() + } +} + +#[repr(transparent)] +#[derive(Clone, PartialEq, Eq, Debug)] +pub struct IAudioPolicyConfig(IInspectable); +impl IAudioPolicyConfig { + pub unsafe fn GetPersistedDefaultAudioEndpoint( + &self, + process_id: u32, + data_flow: EDataFlow, + role: ERole, + ) -> Result { + let mut result__ = MaybeUninit::>::zeroed(); + (Interface::vtable(self).GetPersistedDefaultAudioEndpoint)( + mem::transmute_copy(self), + process_id, + data_flow, + role, + result__.as_mut_ptr(), + ) + .from_abi::(result__) + } + + pub unsafe fn SetPersistedDefaultAudioEndpoint( + &self, + process_id: u32, + data_flow: EDataFlow, + role: ERole, + device_id: HSTRING, + ) -> Result<()> { + (Interface::vtable(self).SetPersistedDefaultAudioEndpoint)( + mem::transmute_copy(self), + process_id, + data_flow, + role, + device_id, + ) + .ok() + } + + pub unsafe fn ClearAllPersistedApplicationDefaultEndpoints(&self) -> Result<()> { + (Interface::vtable(self).ClearAllPersistedApplicationDefaultEndpoints)(mem::transmute_copy( + self, + )) + .ok() + } +} +impl From> for IInspectable { + fn from(value: IAudioPolicyConfig) -> Self { + unsafe { mem::transmute(value) } + } +} + +impl From<&IAudioPolicyConfig> for IInspectable { + fn from(value: &IAudioPolicyConfig) -> Self { + From::from(Clone::clone(value)) + } +} + +impl<'a, const T: u128> IntoParam<'a, IInspectable> for IAudioPolicyConfig { + fn into_param(self) -> Param<'a, IInspectable> { + Param::Owned(unsafe { mem::transmute(self) }) + } +} + +impl<'a, const T: u128> IntoParam<'a, IInspectable> for &IAudioPolicyConfig { + fn into_param(self) -> Param<'a, IInspectable> { + Param::Borrowed(unsafe { mem::transmute(self) }) + } +} + +unsafe impl Interface for IAudioPolicyConfig { + const IID: GUID = GUID::from_u128(T); + type Vtable = IAudioPolicyConfig_Vtbl; +} + +#[repr(C)] +#[doc(hidden)] +pub struct IAudioPolicyConfig_Vtbl { + pub base: IInspectableVtbl, + + pub __incomplete__add_CtxVolumeChanged: unsafe extern "system" fn() -> u32, + pub __incomplete__remove_CtxVolumeChanged: unsafe extern "system" fn() -> u32, + pub __incomplete__add_RingerVibrateStateChanged: unsafe extern "system" fn() -> u32, + pub __incomplete__remove_RingerVibrateStateChanged: unsafe extern "system" fn() -> u32, + pub __incomplete__SetVolumeGroupGainForId: unsafe extern "system" fn() -> u32, + pub __incomplete__GetVolumeGroupGainForId: unsafe extern "system" fn() -> u32, + pub __incomplete__GetActiveVolumeGroupForEndpointId: unsafe extern "system" fn() -> u32, + pub __incomplete__GetVolumeGroupsForEndpoint: unsafe extern "system" fn() -> u32, + pub __incomplete__GetCurrentVolumeContext: unsafe extern "system" fn() -> u32, + pub __incomplete__SetVolumeGroupMuteForId: unsafe extern "system" fn() -> u32, + pub __incomplete__GetVolumeGroupMuteForId: unsafe extern "system" fn() -> u32, + pub __incomplete__SetRingerVibrateState: unsafe extern "system" fn() -> u32, + pub __incomplete__GetRingerVibrateState: unsafe extern "system" fn() -> u32, + pub __incomplete__SetPreferredChatApplication: unsafe extern "system" fn() -> u32, + pub __incomplete__ResetPreferredChatApplication: unsafe extern "system" fn() -> u32, + pub __incomplete__GetPreferredChatApplication: unsafe extern "system" fn() -> u32, + pub __incomplete__GetCurrentChatApplications: unsafe extern "system" fn() -> u32, + pub __incomplete__add_ChatContextChanged: unsafe extern "system" fn() -> u32, + pub __incomplete__remove_ChatContextChanged: unsafe extern "system" fn() -> u32, + + pub SetPersistedDefaultAudioEndpoint: unsafe extern "system" fn( + this: *mut c_void, + process_id: u32, + data_flow: EDataFlow, + role: ERole, + device_id: HSTRING, + ) -> HRESULT, + + pub GetPersistedDefaultAudioEndpoint: unsafe extern "system" fn( + this: *mut c_void, + process_id: u32, + data_flow: EDataFlow, + role: ERole, + device_id_ptr: *mut ManuallyDrop, + ) -> HRESULT, + + pub ClearAllPersistedApplicationDefaultEndpoints: + unsafe extern "system" fn(this: *mut c_void) -> HRESULT, +} + +/// Direction in which audio is moving. +#[derive(Debug)] +pub enum FlowDirection { + /// Audio is being rendered (played). + Render, + /// Audio is being captured. + Capture, +} + +/// Audio device role. +#[derive(Debug)] +pub enum Role { + /// Interaction with the computer. + Console, + /// Playing or recording audio content. + Multimedia, + /// Voice communications with another person. + Communications, +} + +/// State of the device. +#[derive(Debug, Ord, PartialOrd, Eq, PartialEq)] +pub enum DeviceState { + /// The audio endpoint device is active. That is, the audio adapter that + /// connects to the endpoint device is present and enabled. In addition, if + /// the endpoint device plugs into a jack on the adapter, then the endpoint + /// device is plugged in. + Active, + /// The audio endpoint device is disabled. The user has disabled the device + /// in the Windows multimedia control panel. + Disabled, + /// The audio endpoint device is not present because the audio adapter that + /// connects to the endpoint device has been removed from the system, or the + /// user has disabled the adapter device in Device Manager. + NotPresent, + /// The audio endpoint device is unplugged. The audio adapter that contains + /// the jack for the endpoint device is present and enabled, but the + /// endpoint device is not plugged into the jack. Only a device with + /// jack-presence detection can be in this state. + Unplugged, +} + +/// A notification about a device change. +#[derive(Debug)] +pub enum DeviceNotification { + /// The default device has changed. + DefaultDeviceChanged { + /// The flow of the device. + flow_direction: FlowDirection, + /// The role of the device. + role: Role, + /// The device ID. + default_device_id: String, + }, + /// A device was added. + DeviceAdded { + /// The device ID. + device_id: String, + }, + /// A device was removed. + DeviceRemoved { + /// The device ID. + device_id: String, + }, + /// The state of a device changed. + StateChanged { + /// The device ID. + device_id: String, + /// The new device state. + state: DeviceState, + }, + /// A property changed on the device. + PropertyChanged { + /// The device ID. + device_id: String, + /// The property fmtid. + property_key_fmtid: GUID, + /// The property pid. + property_key_pid: u32, + }, +} + +#[repr(C)] +pub(crate) struct DeviceNotifications { + _abi: Box, + ref_cnt: u32, + tx: Sender, +} + +impl DeviceNotifications { + #[allow(clippy::new_ret_no_self)] + pub(crate) fn new(tx: Sender) -> IMMNotificationClient { + let target = Box::new(Self { + _abi: Box::new(IMMNotificationClient_Vtbl { + base__: IUnknownVtbl { + QueryInterface: Self::_query_interface, + AddRef: Self::_add_ref, + Release: Self::_release, + }, + OnDeviceStateChanged: Self::_on_device_state_changed, + OnDeviceAdded: Self::_on_device_added, + OnDeviceRemoved: Self::_on_device_removed, + OnDefaultDeviceChanged: Self::_on_default_device_changed, + OnPropertyValueChanged: Self::_on_property_value_changed, + }), + ref_cnt: 1, + tx, + }); + + unsafe { + let ptr = Box::into_raw(target); + mem::transmute(ptr) + } + } + + fn query_interface(&mut self, iid: &GUID, interface: *mut *const c_void) -> HRESULT { + if iid == &IAudioSessionEvents::IID || iid == &IUnknown::IID { + unsafe { + *interface = self as *mut Self as *mut _; + } + + self.add_ref(); + + S_OK + } else { + E_NOINTERFACE + } + } + + fn add_ref(&mut self) -> u32 { + self.ref_cnt += 1; + self.ref_cnt + } + + fn release(&mut self) -> u32 { + self.ref_cnt -= 1; + + if self.ref_cnt == 0 { + unsafe { + Box::from_raw(self as *mut Self); + } + } + + self.ref_cnt + } + + fn on_default_device_changed( + &mut self, + flow_direction: FlowDirection, + role: Role, + default_device_id: String, + ) { + self.tx + .send(DeviceNotification::DefaultDeviceChanged { + flow_direction, + role, + default_device_id, + }) + .expect("could not send on_default_device_changed"); + } + + fn on_device_added(&mut self, device_id: String) { + self.tx + .send(DeviceNotification::DeviceAdded { device_id }) + .expect("could not send on_device_added"); + } + + fn on_device_removed(&mut self, device_id: String) { + self.tx + .send(DeviceNotification::DeviceRemoved { device_id }) + .expect("could not send on_device_removed"); + } + + fn on_device_state_changed(&mut self, device_id: String, new_state: DeviceState) { + self.tx + .send(DeviceNotification::StateChanged { + device_id, + state: new_state, + }) + .expect("could not send on_device_state_changed"); + } + + fn on_property_value_changed(&mut self, device_id: String, property_key: PROPERTYKEY) { + self.tx + .send(DeviceNotification::PropertyChanged { + device_id, + property_key_fmtid: property_key.fmtid, + property_key_pid: property_key.pid, + }) + .expect("could not send on_property_value_changed"); + } +} + +impl DeviceNotifications { + unsafe extern "system" fn _query_interface( + this: RawPtr, + iid: &GUID, + interface: *mut *const c_void, + ) -> HRESULT { + (*(this as *mut Self)).query_interface(iid, interface) + } + + unsafe extern "system" fn _add_ref(this: RawPtr) -> u32 { + (*(this as *mut Self)).add_ref() + } + + unsafe extern "system" fn _release(this: RawPtr) -> u32 { + (*(this as *mut Self)).release() + } + + unsafe extern "system" fn _on_default_device_changed( + this: RawPtr, + flow: EDataFlow, + role: ERole, + default_device_id: PCWSTR, + ) -> HRESULT { + let default_device_id = U16CStr::from_ptr_str(default_device_id.0).to_string_lossy(); + + #[allow(non_upper_case_globals)] + let flow = match flow { + eRender => FlowDirection::Render, + eCapture => FlowDirection::Capture, + _ => { + warn!("got unknown flow direction {:?}", flow); + return S_OK; + } + }; + + #[allow(non_upper_case_globals)] + let role = match role { + eConsole => Role::Console, + eMultimedia => Role::Multimedia, + eCommunications => Role::Communications, + _ => { + warn!("got unknown role {:?}", role); + return S_OK; + } + }; + + (*(this as *mut Self)).on_default_device_changed(flow, role, default_device_id); + + S_OK + } + + unsafe extern "system" fn _on_device_added(this: RawPtr, device_id: PCWSTR) -> HRESULT { + let device_id = U16CStr::from_ptr_str(device_id.0).to_string_lossy(); + + (*(this as *mut Self)).on_device_added(device_id); + + S_OK + } + + unsafe extern "system" fn _on_device_removed(this: RawPtr, device_id: PCWSTR) -> HRESULT { + let device_id = U16CStr::from_ptr_str(device_id.0).to_string_lossy(); + + (*(this as *mut Self)).on_device_removed(device_id); + + S_OK + } + + unsafe extern "system" fn _on_device_state_changed( + this: RawPtr, + device_id: PCWSTR, + new_state: u32, + ) -> HRESULT { + let device_id = U16CStr::from_ptr_str(device_id.0).to_string_lossy(); + + let new_state = match new_state { + DEVICE_STATE_ACTIVE => DeviceState::Active, + DEVICE_STATE_DISABLED => DeviceState::Disabled, + DEVICE_STATE_NOTPRESENT => DeviceState::NotPresent, + DEVICE_STATE_UNPLUGGED => DeviceState::Unplugged, + _ => { + warn!("got unknown device state: {:?}", new_state); + return S_OK; + } + }; + + (*(this as *mut Self)).on_device_state_changed(device_id, new_state); + + S_OK + } + + unsafe extern "system" fn _on_property_value_changed( + this: RawPtr, + device_id: PCWSTR, + property_key: PROPERTYKEY, + ) -> HRESULT { + let device_id = U16CStr::from_ptr_str(device_id.0).to_string_lossy(); + + (*(this as *mut Self)).on_property_value_changed(device_id, property_key); + + S_OK + } +} + +/// An event for a device. +#[derive(Debug)] +pub struct DeviceEvent { + /// The new volume level, [0, 1]. + pub level: f32, + /// If the device is muted. + pub muted: bool, + + /// The volume for each channel. + pub channel_volumes: Vec, + + /// An event context, if one exists. + pub event_context: GUID, +} + +impl From for DeviceEvent { + fn from(notification_data: AUDIO_VOLUME_NOTIFICATION_DATA) -> Self { + let channel_volumes = unsafe { + std::slice::from_raw_parts( + ¬ification_data.afChannelVolumes as *const _, + notification_data.nChannels as usize, + ) + }; + + Self { + level: notification_data.fMasterVolume, + muted: notification_data.bMuted.into(), + + channel_volumes: channel_volumes.to_vec(), + + event_context: notification_data.guidEventContext, + } + } +} + +/// A notification about an audio session. +#[derive(Debug)] +pub struct AudioSessionNotification { + /// The session identifier. + pub session_identifier: String, + /// The session instance identifier. + pub session_instance_identifier: String, +} + +#[repr(C)] +pub(crate) struct AudioSessionNotifications { + _abi: Box, + ref_cnt: u32, + tx: Sender, +} + +impl AudioSessionNotifications { + #[allow(clippy::new_ret_no_self)] + pub(crate) fn new(tx: Sender) -> IAudioSessionNotification { + let target = Box::new(Self { + _abi: Box::new(IAudioSessionNotification_Vtbl { + base__: IUnknownVtbl { + QueryInterface: Self::_query_interface, + AddRef: Self::_add_ref, + Release: Self::_release, + }, + OnSessionCreated: Self::_on_session_created, + }), + ref_cnt: 1, + tx, + }); + + unsafe { + let ptr = Box::into_raw(target); + mem::transmute(ptr) + } + } + + fn query_interface(&mut self, iid: &GUID, interface: *mut *const c_void) -> HRESULT { + if iid == &IAudioSessionEvents::IID || iid == &IUnknown::IID { + unsafe { + *interface = self as *mut Self as *mut _; + } + + self.add_ref(); + + S_OK + } else { + E_NOINTERFACE + } + } + + fn add_ref(&mut self) -> u32 { + self.ref_cnt += 1; + self.ref_cnt + } + + fn release(&mut self) -> u32 { + self.ref_cnt -= 1; + + if self.ref_cnt == 0 { + unsafe { + Box::from_raw(self as *mut Self); + } + } + + self.ref_cnt + } + + fn on_session_created(&mut self, new_session: IAudioSessionControl2) { + let (session_identifier, session_instance_identifier) = unsafe { + let session_identifier = new_session.GetSessionIdentifier().unwrap_or_default(); + let session_identifier = U16CStr::from_ptr_str(session_identifier.0).to_string_lossy(); + + let session_instance_identifier = new_session + .GetSessionInstanceIdentifier() + .unwrap_or_default(); + let session_instance_identifier = + U16CStr::from_ptr_str(session_instance_identifier.0).to_string_lossy(); + + (session_identifier, session_instance_identifier) + }; + + self.tx + .send(AudioSessionNotification { + session_identifier, + session_instance_identifier, + }) + .expect("could not send on_session_created"); + } +} + +impl AudioSessionNotifications { + unsafe extern "system" fn _query_interface( + this: RawPtr, + iid: &GUID, + interface: *mut *const c_void, + ) -> HRESULT { + (*(this as *mut Self)).query_interface(iid, interface) + } + + unsafe extern "system" fn _add_ref(this: RawPtr) -> u32 { + (*(this as *mut Self)).add_ref() + } + + unsafe extern "system" fn _release(this: RawPtr) -> u32 { + (*(this as *mut Self)).release() + } + + unsafe extern "system" fn _on_session_created(this: RawPtr, new_session: RawPtr) -> HRESULT { + struct ComObject(RawPtr); + let obj = ComObject(new_session); + let sess = (&*(&obj as *const _ as *const IAudioSessionControl)).clone(); + + let new_session = if let Ok(control) = sess.cast() { + control + } else { + warn!("could not cast NewSession to IAudioSessionControl2"); + return S_OK; + }; + + (*(this as *mut Self)).on_session_created(new_session); + + S_OK + } +} + +/// The type of change for an audio session. +#[derive(Debug)] +pub enum AudioSessionEvent { + /// The volume or mute status has changed. + VolumeChange { + /// The new volume level, [0, 1]. + level: f32, + /// If the session is muted. + muted: bool, + }, + /// The state of the session has changed. + StateChange(SessionState), + /// The session has disconnected. + Disconnect(SessionDisconnect), +} + +/// An audio session state. +#[derive(Copy, Clone, Debug, Ord, PartialOrd, Eq, PartialEq)] +pub enum SessionState { + /// The audio session is currently active. + Active, + /// The audio session has become inactive. + Inactive, + /// The audio session has expired and is no longer valid. + Expired, +} + +/// The reason why an audio session disconnected. +#[derive(Debug, Copy, Clone, Ord, PartialOrd, Eq, PartialEq)] +pub enum SessionDisconnect { + /// The user removed the audio endpoint device. + DeviceRemoved, + /// The Windows audio service has stopped. + ServerShutdown, + /// The stream format changed for the device that the audio session is + /// connected to. + FormatChanged, + /// The user logged off the Windows Terminal Services (WTS) session that the + /// audio session was running in. + SessionLogoff, + /// The WTS session that the audio session was running in was disconnected. + SessionDisconnected, + /// The (shared-mode) audio session was disconnected to make the audio + /// endpoint device available for an exclusive-mode connection. + ExclusiveModeOverride, +} + +#[repr(C)] +pub(crate) struct AudioSessionEvents { + _abi: Box, + ref_cnt: u32, + + tx: Sender, +} + +impl AudioSessionEvents { + pub(crate) fn create(tx: Sender) -> IAudioSessionEvents { + let target = Box::new(Self { + _abi: Box::new(IAudioSessionEvents_Vtbl { + base__: IUnknownVtbl { + QueryInterface: Self::_query_interface, + AddRef: Self::_add_ref, + Release: Self::_release, + }, + OnDisplayNameChanged: Self::_on_display_name_changed, + OnIconPathChanged: Self::_on_icon_path_changed, + OnSimpleVolumeChanged: Self::_on_simple_volume_changed, + OnChannelVolumeChanged: Self::_on_channel_volume_changed, + OnGroupingParamChanged: Self::_on_grouping_param_changed, + OnStateChanged: Self::_on_state_changed, + OnSessionDisconnected: Self::_on_session_disconnected, + }), + ref_cnt: 1, + tx, + }); + + unsafe { + let ptr = Box::into_raw(target); + mem::transmute(ptr) + } + } + + fn query_interface(&mut self, iid: &GUID, interface: *mut *const c_void) -> HRESULT { + if iid == &IAudioSessionEvents::IID || iid == &IUnknown::IID { + unsafe { + *interface = self as *mut Self as *mut _; + } + + self.add_ref(); + + S_OK + } else { + E_NOINTERFACE + } + } + + fn add_ref(&mut self) -> u32 { + self.ref_cnt += 1; + self.ref_cnt + } + + fn release(&mut self) -> u32 { + self.ref_cnt -= 1; + + if self.ref_cnt == 0 { + unsafe { + Box::from_raw(self as *mut Self); + } + } + + self.ref_cnt + } + + fn simple_volume_changed(&mut self, new_volume: f32, new_mute: bool) { + self.tx + .send(AudioSessionEvent::VolumeChange { + level: new_volume, + muted: new_mute, + }) + .expect("could not send simple_volume_changed"); + } + + fn on_state_changed(&mut self, state: SessionState) { + self.tx + .send(AudioSessionEvent::StateChange(state)) + .expect("could not send on_state_changed"); + } + + fn on_session_disconnected(&mut self, session_disconnect: SessionDisconnect) { + self.tx + .send(AudioSessionEvent::Disconnect(session_disconnect)) + .expect("could not send on_session_disconnected"); + } +} + +/// Methods called by Windows API. +impl AudioSessionEvents { + unsafe extern "system" fn _query_interface( + this: RawPtr, + iid: &GUID, + interface: *mut *const c_void, + ) -> HRESULT { + (*(this as *mut Self)).query_interface(iid, interface) + } + + unsafe extern "system" fn _add_ref(this: RawPtr) -> u32 { + (*(this as *mut Self)).add_ref() + } + + unsafe extern "system" fn _release(this: RawPtr) -> u32 { + (*(this as *mut Self)).release() + } + + unsafe extern "system" fn _on_display_name_changed( + _this: RawPtr, + _new_display_name: PCWSTR, + _event_context: *const GUID, + ) -> HRESULT { + S_OK + } + + unsafe extern "system" fn _on_icon_path_changed( + _this: RawPtr, + _new_icon_path: PCWSTR, + _event_context: *const GUID, + ) -> HRESULT { + S_OK + } + + unsafe extern "system" fn _on_simple_volume_changed( + this: RawPtr, + new_volume: f32, + new_mute: BOOL, + _event_context: *const GUID, + ) -> HRESULT { + (*(this as *mut Self)).simple_volume_changed(new_volume, new_mute.into()); + + S_OK + } + + unsafe extern "system" fn _on_channel_volume_changed( + _this: RawPtr, + _channel_count: u32, + _new_channel_volume_array: *const f32, + _changed_channel: u32, + _event_context: *const GUID, + ) -> HRESULT { + S_OK + } + + unsafe extern "system" fn _on_grouping_param_changed( + _this: RawPtr, + _new_grouping_param: *const GUID, + _event_context: *const GUID, + ) -> HRESULT { + S_OK + } + + unsafe extern "system" fn _on_state_changed( + this: RawPtr, + new_state: AudioSessionState, + ) -> HRESULT { + #[allow(non_upper_case_globals)] + let state = match new_state { + AudioSessionStateActive => SessionState::Active, + AudioSessionStateInactive => SessionState::Inactive, + AudioSessionStateExpired => SessionState::Expired, + _ => { + warn!("got unknown state"); + return S_OK; + } + }; + + (*(this as *mut Self)).on_state_changed(state); + + S_OK + } + + unsafe extern "system" fn _on_session_disconnected( + this: RawPtr, + disconnect_reason: AudioSessionDisconnectReason, + ) -> HRESULT { + #[allow(non_upper_case_globals)] + let session_disconnect = match disconnect_reason { + DisconnectReasonDeviceRemoval => SessionDisconnect::DeviceRemoved, + DisconnectReasonServerShutdown => SessionDisconnect::ServerShutdown, + DisconnectReasonFormatChanged => SessionDisconnect::FormatChanged, + DisconnectReasonSessionLogoff => SessionDisconnect::SessionLogoff, + DisconnectReasonSessionDisconnected => SessionDisconnect::SessionDisconnected, + DisconnectReasonExclusiveModeOverride => SessionDisconnect::ExclusiveModeOverride, + _ => { + warn!("got unknown disconnect reason"); + return S_OK; + } + }; + + (*(this as *mut Self)).on_session_disconnected(session_disconnect); + + S_OK + } +} diff --git a/crates/nodio-win32/src/device.rs b/crates/nodio-win32/src/device.rs new file mode 100644 index 0000000..276665f --- /dev/null +++ b/crates/nodio-win32/src/device.rs @@ -0,0 +1,335 @@ +use std::mem::MaybeUninit; +use std::ptr::null; +use std::str::FromStr; +use std::sync::mpsc::channel; +use std::sync::Arc; +use std::time::Duration; + +use log::{error, trace, warn}; +use notify_thread::JoinHandle; +use parking_lot::Mutex; +use widestring::U16Str; +use windows::core::{Interface, GUID, HSTRING, PWSTR}; +use windows::Win32::Devices::FunctionDiscovery::PKEY_Device_FriendlyName; +use windows::Win32::Media::Audio as windows_audio; +use windows::Win32::Media::Audio::Endpoints::{IAudioEndpointVolume, IAudioMeterInformation}; +use windows::Win32::Media::Audio::{ + EDataFlow, IAudioSessionControl, IAudioSessionEnumerator, IAudioSessionManager2, + IAudioSessionNotification, IMMDevice, +}; +use windows::Win32::System::Com::StructuredStorage::{PROPVARIANT, STGM_READ, STGM_WRITE}; +use windows::Win32::System::Com::CLSCTX_ALL; +use windows::Win32::System::Ole::{VT_BOOL, VT_LPWSTR}; +use windows::Win32::UI::Shell::PropertiesSystem::{IPropertyStore, PropVariantToBSTR, PROPERTYKEY}; + +use nodio_core::Uuid; + +use crate::custom::{AudioSessionNotification, AudioSessionNotifications, DeviceState}; +use crate::session::AudioSession; +use crate::{pwstr_to_string, Callback}; + +pub const DEVINTERFACE_AUDIO_RENDER: &str = "{e6327cad-dcec-4949-ae8a-991e976a79d2}"; +pub const DEVINTERFACE_AUDIO_CAPTURE: &str = "{2eef81be-33fa-4800-9670-1cd474972c3f}"; +pub const MMDEVAPI_TOKEN: &str = r#"\\?\SWD#MMDEVAPI#{0.0.0.00000000}."#; + +pub struct AudioDevice { + mmdevice: IMMDevice, + + audio_session_manager: IAudioSessionManager2, + session_notifications: IAudioSessionNotification, + endpoint_volume: Option, + meter: Option, + name: String, + + id: Uuid, + + session_notification_callback: Arc>>>, + + session_notification_thread: Option>, +} + +impl Drop for AudioDevice { + fn drop(&mut self) { + trace!("dropping audio device {}", self.name); + unsafe { + self.audio_session_manager + .UnregisterSessionNotification(self.session_notifications.clone()) + .ok(); + } + if let Some(t) = self.session_notification_thread.take() { + t.notify(); + } + trace!("audio device dropped"); + } +} + +impl AudioDevice { + pub fn new(mmdevice: IMMDevice) -> windows::core::Result { + unsafe { + let audio_session_manager = mmdevice.activate::()?; + + let (session_notification_tx, session_notification_rx) = channel(); + let session_notifications = AudioSessionNotifications::new(session_notification_tx); + audio_session_manager + .RegisterSessionNotification(session_notifications.clone()) + .unwrap(); + + let session_notification_callback: Arc< + Mutex>>, + > = Arc::new(Mutex::new(None)); + + let session_notification_thread = { + let session_notification_callback = session_notification_callback.clone(); + notify_thread::spawn(move |thread| loop { + match session_notification_rx.recv_timeout(Duration::from_millis(100)) { + Ok(event) => { + trace!("Device session event: {:?}", event); + + if let Some(cb) = session_notification_callback.lock().as_ref() { + cb(event); + } + } + _ if thread.notified() => { + trace!("Session notification thread ended"); + return; + } + _ => {} + } + }) + }; + + let properties: IPropertyStore = mmdevice.OpenPropertyStore(STGM_READ)?; + let name: PROPVARIANT = properties.GetValue(&PKEY_Device_FriendlyName)?; + let name = U16Str::from_slice(PropVariantToBSTR(&name)?.as_wide()).to_string_lossy(); + + let id = mmdevice.GetId().map(|id| { + if id.is_null() { + Uuid::nil() + } else { + pwstr_to_string(id) + .split_once("}.{") + .and_then(|(_, s)| s.split('}').next()) + .and_then(|s| Uuid::from_str(s).ok()) + .unwrap_or_else(Uuid::nil) + } + })?; + + let endpoint_volume: Option = mmdevice + .GetState() + .ok() + .filter(|state| *state == windows_audio::DEVICE_STATE_ACTIVE) + .and_then(|_| mmdevice.activate().ok()); + + let meter: Option = mmdevice + .GetState() + .ok() + .filter(|state| *state == windows_audio::DEVICE_STATE_ACTIVE) + .and_then(|_| mmdevice.activate().ok()); + + Ok(Self { + mmdevice, + audio_session_manager, + endpoint_volume, + session_notifications, + name, + id, + session_notification_callback, + session_notification_thread: Some(session_notification_thread), + meter, + }) + } + } + + pub fn name(&self) -> &str { + &self.name + } + + pub fn mmdevice(&self) -> &IMMDevice { + &self.mmdevice + } + + pub fn set_session_notification_callback(&mut self, cb: T) + where + T: Fn(AudioSessionNotification) + Send + Sync + 'static, + { + let _ = self + .session_notification_callback + .lock() + .insert(Box::new(cb)); + } + + pub fn set_listen(&self, target: Option<&AudioDevice>) -> windows::core::Result<()> { + unsafe { + let listen_state_prop_key = PROPERTYKEY { + fmtid: GUID::from_u128(0x24DBB0FC_9311_4B3D_9CF0_18FF155639D4), + pid: 1u32, + }; + + let listen_target_prop_key = PROPERTYKEY { + fmtid: GUID::from_u128(0x24DBB0FC_9311_4B3D_9CF0_18FF155639D4), + pid: 0u32, + }; + + let properties: IPropertyStore = self.mmdevice.OpenPropertyStore(STGM_WRITE)?; + + let mut listen_state_prop_value: PROPVARIANT = PROPVARIANT::default(); + (*(listen_state_prop_value.Anonymous.Anonymous)).vt = VT_BOOL.0 as _; + (*(listen_state_prop_value.Anonymous.Anonymous)) + .Anonymous + .boolVal = if target.is_some() { -1 } else { 0 }; + + if let Some(target) = target { + let mut target_device_id = + HSTRING::from(format!("{}.{{{}}}", "{0.0.0.00000000}", target.id())) + .as_wide() + .to_vec(); + let mut listen_target_prop_value: PROPVARIANT = PROPVARIANT::default(); + (*(listen_target_prop_value.Anonymous.Anonymous)).vt = VT_LPWSTR.0 as _; + (*(listen_target_prop_value.Anonymous.Anonymous)) + .Anonymous + .pwszVal = PWSTR(target_device_id.as_mut_ptr()); + + properties.SetValue(&listen_target_prop_key, &listen_target_prop_value)?; + } + + properties.SetValue(&listen_state_prop_key, &listen_state_prop_value)?; + + properties.Commit()?; + } + + Ok(()) + } + + pub fn enumerate_sessions(&self) -> windows::core::Result> { + unsafe { + let session_enumerator: IAudioSessionEnumerator = self + .audio_session_manager + .GetSessionEnumerator() + .map_err(|err| { + error!("Failed to get session enumerator: {:?}", err); + err + })?; + + let session_count = session_enumerator.GetCount().map_err(|err| { + error!("Failed to get session count: {:?}", err); + err + })?; + + let mut sessions = Vec::with_capacity(session_count as usize); + + for i in 0..session_count { + let control: IAudioSessionControl = match session_enumerator.GetSession(i) { + Ok(control) => control, + Err(err) => { + error!("Failed to get session control for session {}: {:?}", i, err); + continue; + } + }; + + let session = match AudioSession::new(control) { + Ok(session) => session, + Err(err) => { + error!("Failed to create session {}: {:?}", i, err); + continue; + } + }; + + if session.process_id() == 0 { + continue; + } + + sessions.push(session); + } + + Ok(sessions) + } + } + + pub fn is_active(&self) -> bool { + self.state() == DeviceState::Active + } + + pub fn state(&self) -> DeviceState { + match unsafe { self.mmdevice.GetState() }.unwrap_or(windows_audio::DEVICE_STATE_DISABLED) { + windows_audio::DEVICE_STATE_ACTIVE => DeviceState::Active, + windows_audio::DEVICE_STATE_DISABLED => DeviceState::Disabled, + windows_audio::DEVICE_STATE_NOTPRESENT => DeviceState::NotPresent, + windows_audio::DEVICE_STATE_UNPLUGGED => DeviceState::Unplugged, + _ => DeviceState::Disabled, + } + } + + pub fn id(&self) -> Uuid { + self.id + } + + pub fn set_master_volume(&self, volume: f32) { + unsafe { + if let Some(endpoint_volume) = self.endpoint_volume.as_ref() { + if let Err(err) = endpoint_volume.SetMasterVolumeLevelScalar(volume, null()) { + warn!("Failed to get audio endpoint volume: {}", err); + } + } + } + } + pub fn master_volume(&self) -> f32 { + unsafe { + self.endpoint_volume + .as_ref() + .and_then(|endpoint_volume| endpoint_volume.GetMasterVolumeLevelScalar().ok()) + .unwrap_or(0.0) + } + } + + pub fn peak_values(&self) -> windows::core::Result<(f32, f32)> { + let meter = match self.meter.as_ref() { + Some(meter) => meter, + None => return Ok((0.0, 0.0)), + }; + + unsafe { + let channel_count = usize::min(2, meter.GetMeteringChannelCount()? as usize); + + let mut values = [0.0; 2]; + meter.GetChannelsPeakValues(&mut values)?; + + if channel_count == 1 { + Ok((values[0], values[0])) + } else { + Ok((values[0], values[1])) + } + } + } + + pub fn mmdevice_id(&self, data_flow: EDataFlow) -> HSTRING { + if self.id.is_nil() { + HSTRING::new() + } else { + HSTRING::from(format!( + r#"{}{{{}}}#{}"#, + MMDEVAPI_TOKEN, + self.id, + match data_flow { + windows_audio::eCapture => DEVINTERFACE_AUDIO_CAPTURE, + _ => DEVINTERFACE_AUDIO_RENDER, + } + )) + } + } +} + +pub trait MMDeviceExt { + fn activate(&self) -> windows::core::Result; +} + +impl MMDeviceExt for IMMDevice { + fn activate(&self) -> windows::core::Result { + unsafe { + let mut result = MaybeUninit::::uninit(); + + self.Activate(&T::IID, CLSCTX_ALL, null(), result.as_mut_ptr() as _)?; + + Ok(result.assume_init()) + } + } +} diff --git a/crates/nodio-win32/src/enumerator.rs b/crates/nodio-win32/src/enumerator.rs new file mode 100644 index 0000000..4b846c7 --- /dev/null +++ b/crates/nodio-win32/src/enumerator.rs @@ -0,0 +1,88 @@ +use crate::com::ensure_com_initialized; +use crate::custom::{DeviceNotification, DeviceNotifications}; +use crate::device::AudioDevice; +use log::{trace, warn}; +use std::sync::mpsc::channel; +use std::thread; +use windows::Win32::Media::Audio::{ + EDataFlow, ERole, IMMDeviceCollection, IMMDeviceEnumerator, IMMNotificationClient, + MMDeviceEnumerator, +}; +use windows::Win32::System::Com::{CoCreateInstance, CLSCTX_ALL}; + +pub struct AudioDeviceEnumerator { + enumerator: IMMDeviceEnumerator, + _device_notifications: IMMNotificationClient, +} + +impl AudioDeviceEnumerator { + pub fn create() -> windows::core::Result { + ensure_com_initialized(); + + unsafe { + let enumerator: IMMDeviceEnumerator = + CoCreateInstance(&MMDeviceEnumerator, None, CLSCTX_ALL)?; + + let (device_notification_tx, device_notification_rx) = channel::(); + let device_notifications = DeviceNotifications::new(device_notification_tx); + + enumerator + .RegisterEndpointNotificationCallback(device_notifications.clone()) + .expect("Failed to register endpoint notification callback"); + + thread::spawn(move || { + while let Ok(event) = device_notification_rx.recv() { + trace!("Device event: {:?}", event); + + match event { + DeviceNotification::DefaultDeviceChanged { .. } => {} + DeviceNotification::DeviceAdded { .. } => {} + DeviceNotification::DeviceRemoved { .. } => {} + DeviceNotification::StateChanged { .. } => {} + DeviceNotification::PropertyChanged { .. } => {} + } + } + }); + + Ok(Self { + enumerator, + _device_notifications: device_notifications, + }) + } + } + + pub fn _default_audio_endpoint( + &self, + data_flow: EDataFlow, + role: ERole, + ) -> windows::core::Result { + unsafe { + self.enumerator + .GetDefaultAudioEndpoint(data_flow, role) + .and_then(AudioDevice::new) + } + } + + pub fn enumerate_audio_endpoints( + &self, + data_flow: EDataFlow, + state_mask: u32, + ) -> windows::core::Result> { + unsafe { + let collection: IMMDeviceCollection = + self.enumerator.EnumAudioEndpoints(data_flow, state_mask)?; + + let count = collection.GetCount()?; + let mut endpoints = Vec::with_capacity(count as usize); + + for i in 0..count { + match collection.Item(i).and_then(AudioDevice::new) { + Ok(device) => endpoints.push(device), + Err(err) => warn!("Could not get audio endpoint: {:?}", err), + } + } + + Ok(endpoints) + } + } +} diff --git a/crates/nodio-win32/src/lib.rs b/crates/nodio-win32/src/lib.rs new file mode 100644 index 0000000..698e3b3 --- /dev/null +++ b/crates/nodio-win32/src/lib.rs @@ -0,0 +1,25 @@ +#![deny(clippy::all)] +mod com; +mod context; +mod custom; +mod device; +mod enumerator; +mod loopback; +mod node; +mod render; +mod session; + +use widestring::U16CStr; +use windows::core::PWSTR; + +pub use context::Win32Context; + +fn pwstr_to_string(pwstr: PWSTR) -> String { + if pwstr.is_null() { + String::default() + } else { + unsafe { U16CStr::from_ptr_str(pwstr.0).to_string_lossy() } + } +} + +type Callback = Box; diff --git a/crates/nodio-win32/src/loopback.rs b/crates/nodio-win32/src/loopback.rs new file mode 100644 index 0000000..5face05 --- /dev/null +++ b/crates/nodio-win32/src/loopback.rs @@ -0,0 +1,412 @@ +use parking_lot::Mutex; +use std::future::Future; +use std::mem::ManuallyDrop; +use std::pin::Pin; +use std::ptr::{null, null_mut}; +use std::sync::atomic::{AtomicBool, Ordering}; +use std::sync::mpsc::Receiver; +use std::sync::{mpsc, Arc}; +use std::task::{Context, Poll, Waker}; + +use crate::render::RenderClient; +use nodio_core::Uuid; +use pollster::FutureExt as _; +use windows::core::{implement, IUnknown, Interface, Result, GUID, HRESULT}; +use windows::Win32::Foundation::HANDLE; +use windows::Win32::Media::Audio::{ + ActivateAudioInterfaceAsync, IActivateAudioInterfaceAsyncOperation, + IActivateAudioInterfaceCompletionHandler, IActivateAudioInterfaceCompletionHandler_Impl, + IMMDevice, AUDCLNT_STREAMFLAGS_AUTOCONVERTPCM, AUDCLNT_STREAMFLAGS_EVENTCALLBACK, + AUDCLNT_STREAMFLAGS_LOOPBACK, AUDIOCLIENT_ACTIVATION_PARAMS, AUDIOCLIENT_ACTIVATION_PARAMS_0, + AUDIOCLIENT_ACTIVATION_TYPE_PROCESS_LOOPBACK, AUDIOCLIENT_PROCESS_LOOPBACK_PARAMS, + PROCESS_LOOPBACK_MODE_EXCLUDE_TARGET_PROCESS_TREE, + PROCESS_LOOPBACK_MODE_INCLUDE_TARGET_PROCESS_TREE, VIRTUAL_AUDIO_DEVICE_PROCESS_LOOPBACK, + WAVEFORMATEXTENSIBLE, +}; +use windows::Win32::System::Com::StructuredStorage::{ + PROPVARIANT_0, PROPVARIANT_0_0, PROPVARIANT_0_0_0, +}; +use windows::Win32::System::Com::BLOB; +use windows::Win32::System::Ole::VT_BLOB; +use windows::Win32::System::Threading::CreateEventW; +use windows::Win32::{ + Media::Audio::{IAudioCaptureClient, IAudioClient, AUDCLNT_SHAREMODE_SHARED}, + Media::MediaFoundation::*, + System::Com::StructuredStorage::PROPVARIANT, +}; + +pub struct LoopbackCapture { + target_pid: u32, + include_process_tree: bool, + + format: WAVEFORMATEXTENSIBLE, + + sample_ready_key: u64, + audio_client: Option, + capture_client: Option, + ev_sample_ready: HANDLE, + sample_ready_result: Option, + + queue_id: u32, +} + +impl LoopbackCapture { + fn new(target_pid: u32, format: WAVEFORMATEXTENSIBLE) -> Self { + Self { + format, + target_pid, + include_process_tree: true, + sample_ready_key: 0, + audio_client: None, + capture_client: None, + ev_sample_ready: HANDLE(0), + sample_ready_result: None, + queue_id: 0, + } + } + + pub unsafe fn get_next_packet_size(&self) -> Result { + self.capture_client.as_ref().unwrap().GetNextPacketSize() + } + + pub unsafe fn get_buffer(&mut self) -> Result { + let mut data_ptr = null_mut::(); + + let mut frames: u32 = 0; + let mut dw_capture_flags: u32 = 0; + let mut device_position: u64 = 0; + let mut qpc_position: u64 = 0; + + self.capture_client.as_ref().unwrap().GetBuffer( + &mut data_ptr as *mut *mut u8, + &mut frames as *mut u32, + &mut dw_capture_flags as *mut u32, + &mut device_position as *mut u64, + &mut qpc_position as *mut u64, + )?; + + let num_block_align: u16 = + self.format.Format.nChannels * self.format.Format.wBitsPerSample / 8u16; + + Ok(BufferPacket { + data: data_ptr, + frames, + size: frames * num_block_align as u32, + }) + } + + pub unsafe fn release_buffer(&mut self, frames: u32) -> Result<()> { + self.capture_client + .as_ref() + .unwrap() + .ReleaseBuffer(frames)?; + + self.sample_ready_key = + MFPutWaitingWorkItem(self.ev_sample_ready, 0, &self.sample_ready_result)?; + + Ok(()) + } + + pub unsafe fn start(&mut self, callback: Box) { + let mut task_id: u32 = 0; + + MFStartup(MF_SDK_VERSION << 16 | MF_API_VERSION, MFSTARTUP_LITE).unwrap(); + MFLockSharedWorkQueue("Capture", 0, &mut task_id, &mut self.queue_id).unwrap(); + + let mut audio_params = AUDIOCLIENT_ACTIVATION_PARAMS { + ActivationType: AUDIOCLIENT_ACTIVATION_TYPE_PROCESS_LOOPBACK, + Anonymous: AUDIOCLIENT_ACTIVATION_PARAMS_0 { + ProcessLoopbackParams: AUDIOCLIENT_PROCESS_LOOPBACK_PARAMS { + TargetProcessId: self.target_pid, + ProcessLoopbackMode: if self.include_process_tree { + PROCESS_LOOPBACK_MODE_INCLUDE_TARGET_PROCESS_TREE + } else { + PROCESS_LOOPBACK_MODE_EXCLUDE_TARGET_PROCESS_TREE + }, + }, + }, + }; + + let activate_params = ManuallyDrop::new(PROPVARIANT_0_0 { + vt: VT_BLOB.0 as u16, + Anonymous: PROPVARIANT_0_0_0 { + blob: BLOB { + cbSize: std::mem::size_of::() as u32, + pBlobData: (&mut audio_params) as *mut AUDIOCLIENT_ACTIVATION_PARAMS as *mut u8, + }, + }, + ..Default::default() + }); + + let activate_params: PROPVARIANT = PROPVARIANT { + Anonymous: PROPVARIANT_0 { + Anonymous: activate_params, + }, + }; + + let completion_handler = CompletionHandler::new(); + let completion_handler_interface: IActivateAudioInterfaceCompletionHandler = + completion_handler.clone().into(); + + let op = ActivateAudioInterfaceAsync( + VIRTUAL_AUDIO_DEVICE_PROCESS_LOOPBACK, + &IAudioClient::IID as *const GUID, + &activate_params, + &completion_handler_interface, + ) + .unwrap(); + + completion_handler.block_on(); + + let mut activate_result = HRESULT(0); + let mut activated_interface: Option = None; + + op.GetActivateResult( + &mut activate_result as *mut HRESULT, + &mut activated_interface as *mut Option, + ) + .unwrap(); + + activate_result.ok().unwrap(); + + let activated_interface = activated_interface.unwrap(); + let audio_client: IAudioClient = core::mem::transmute(activated_interface); + self.audio_client = Some(audio_client); + let audio_client = self.audio_client.as_ref().unwrap(); + + audio_client + .Initialize( + AUDCLNT_SHAREMODE_SHARED, + AUDCLNT_STREAMFLAGS_LOOPBACK + | AUDCLNT_STREAMFLAGS_EVENTCALLBACK + | AUDCLNT_STREAMFLAGS_AUTOCONVERTPCM, + 0, + 0, + &self.format as *const WAVEFORMATEXTENSIBLE as _, //capture_format, + null(), + ) + .unwrap(); + + let capture_client = audio_client.GetService::().unwrap(); + self.capture_client = Some(capture_client); + + let sample_capturer: IMFAsyncCallback = AsyncCallback::create( + self.queue_id, + Some(callback), + self as *const LoopbackCapture as *mut LoopbackCapture, + ); + + let ev_sample_ready = CreateEventW(null(), false, false, None).unwrap(); + + let async_result = MFCreateAsyncResult(None, &sample_capturer, None).unwrap(); + self.sample_ready_result = Some(async_result); + + audio_client.SetEventHandle(ev_sample_ready).unwrap(); + + let (start_capture, receiver) = + AsyncCallback::with_receiver(MFASYNC_CALLBACK_QUEUE_MULTITHREADED); + + MFPutWorkItem2( + MFASYNC_CALLBACK_QUEUE_MULTITHREADED, + 0, + &start_capture, + None, + ) + .unwrap(); + + receiver.recv().ok(); + + audio_client.Start().unwrap(); + + self.sample_ready_key = + MFPutWaitingWorkItem(ev_sample_ready, 0, &self.sample_ready_result).unwrap(); + + self.ev_sample_ready = ev_sample_ready; + } + + pub unsafe fn stop(&mut self) { + if self.sample_ready_key != 0 { + MFCancelWorkItem(self.sample_ready_key).unwrap(); + self.sample_ready_key = 0; + } + + if let Some(client) = &self.audio_client { + client.Stop().unwrap(); + self.audio_client = None; + } + + self.sample_ready_result = None; + + if self.queue_id != 0 { + MFUnlockWorkQueue(self.queue_id).unwrap(); + self.queue_id = 0; + } + } +} + +#[implement(IActivateAudioInterfaceCompletionHandler)] +#[derive(Clone)] +struct CompletionHandler { + completed: Arc, + waker: Arc>>, +} + +impl CompletionHandler { + fn new() -> CompletionHandler { + CompletionHandler { + completed: Arc::new(AtomicBool::new(false)), + waker: Arc::new(Mutex::new(None)), + } + } +} + +impl IActivateAudioInterfaceCompletionHandler_Impl for CompletionHandler { + fn ActivateCompleted(&self, _: &Option) -> Result<()> { + let self_ptr = self as *const CompletionHandler as *mut CompletionHandler; + unsafe { + (*self_ptr).completed.store(true, Ordering::SeqCst); + } + + if let Some(waker) = self.waker.lock().take() { + waker.wake() + }; + + Ok(()) + } +} + +impl Future for CompletionHandler { + type Output = (); + + fn poll(self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll { + if self.completed.load(Ordering::SeqCst) { + Poll::Ready(()) + } else { + self.waker.lock().replace(cx.waker().clone()); + Poll::Pending + } + } +} + +#[implement(IMFAsyncCallback)] +struct AsyncCallback { + queue_id: u32, + sender: Option>, + callback: Option>, + capture_ptr: *mut LoopbackCapture, +} + +impl AsyncCallback { + fn create( + queue_id: u32, + callback: Option>, + capture_ptr: *mut LoopbackCapture, + ) -> IMFAsyncCallback { + AsyncCallback { + queue_id, + sender: None, + callback, + capture_ptr, + } + .into() + } + + fn with_receiver(queue_id: u32) -> (IMFAsyncCallback, Receiver<()>) { + let (tx, rx) = mpsc::channel(); + ( + AsyncCallback { + queue_id, + sender: Some(tx), + callback: None, + capture_ptr: null_mut(), + } + .into(), + rx, + ) + } +} + +impl IMFAsyncCallback_Impl for AsyncCallback { + fn GetParameters(&self, pdwflags: *mut u32, pdwqueue: *mut u32) -> Result<()> { + unsafe { + *pdwflags = 0; + *pdwqueue = self.queue_id; + } + Ok(()) + } + + fn Invoke(&self, _result: &Option) -> Result<()> { + if let Some(sender) = self.sender.as_ref() { + sender.send(()).expect("send() failed."); + } + + if let Some(c) = self.callback.as_ref() { + c(unsafe { &mut *self.capture_ptr }); + } + Ok(()) + } +} + +#[repr(C)] +pub struct BufferPacket { + pub data: *const u8, + pub frames: u32, + pub size: u32, +} + +pub struct LoopbackSession { + pub src_id: Uuid, + pub dst_id: Uuid, + capture: Box, +} + +impl Drop for LoopbackSession { + fn drop(&mut self) { + unsafe { + self.capture.stop(); + } + } +} + +impl LoopbackSession { + pub fn start( + src_id: Uuid, + dst_id: Uuid, + process_id: u32, + target_device: &IMMDevice, + ) -> Result { + let render_client = RenderClient::new(target_device)?; + let mut capture = Box::new(LoopbackCapture::new( + process_id, + *render_client.wave_format(), + )); + + let frame_callback = Box::new(move |capture: &mut LoopbackCapture| unsafe { + let frames = capture + .get_next_packet_size() + .expect("Failed to get next packet size"); + + if frames == 0 { + return; + } + + let packet = capture.get_buffer().expect("Failed to get buffer"); + + render_client.render_frames(packet.data, packet.frames).ok(); + + capture + .release_buffer(frames) + .expect("Failed to release buffer"); + }); + + unsafe { + capture.start(frame_callback); + } + + Ok(Self { + src_id, + dst_id, + capture, + }) + } +} diff --git a/crates/nodio-win32/src/node.rs b/crates/nodio-win32/src/node.rs new file mode 100644 index 0000000..50633f9 --- /dev/null +++ b/crates/nodio-win32/src/node.rs @@ -0,0 +1,16 @@ +use nodio_core::Uuid; + +#[derive(Debug, Copy, Clone, Ord, PartialOrd, Eq, PartialEq)] +pub enum NodeConnectionKind { + DefaultEndpoint, + Loopback, + Listen, +} + +#[derive(Debug, Copy, Clone)] +pub struct NodeConnectionInfo { + pub id: Uuid, + pub src_id: Uuid, + pub dst_id: Uuid, + pub kind: NodeConnectionKind, +} diff --git a/crates/nodio-win32/src/render.rs b/crates/nodio-win32/src/render.rs new file mode 100644 index 0000000..57ddad3 --- /dev/null +++ b/crates/nodio-win32/src/render.rs @@ -0,0 +1,70 @@ +use crate::device::MMDeviceExt; +use log::warn; +use std::ptr::null; +use windows::Win32::Media::Audio::{ + IAudioClient, IAudioRenderClient, IMMDevice, AUDCLNT_SHAREMODE_SHARED, WAVEFORMATEX, + WAVEFORMATEXTENSIBLE, +}; +use windows::Win32::Media::KernelStreaming::WAVE_FORMAT_EXTENSIBLE; + +pub struct RenderClient { + audio_client: IAudioClient, + render_client: IAudioRenderClient, + wave_format: WAVEFORMATEXTENSIBLE, +} + +impl Drop for RenderClient { + fn drop(&mut self) { + if let Err(err) = unsafe { self.audio_client.Stop() } { + warn!("Could not stop render client: {}", err); + } + } +} + +impl RenderClient { + pub fn new(device: &IMMDevice) -> windows::core::Result { + unsafe { + let audio_client = device.activate::()?; + let pwfx: *mut WAVEFORMATEX = audio_client.GetMixFormat()?; + audio_client.Initialize(AUDCLNT_SHAREMODE_SHARED, 0, 0, 0, pwfx, null())?; + let render_client = audio_client.GetService::()?; + + let mut wave_format: WAVEFORMATEXTENSIBLE = std::mem::zeroed(); + + if (*pwfx).wFormatTag == WAVE_FORMAT_EXTENSIBLE as _ { + wave_format = *(pwfx as *mut WAVEFORMATEXTENSIBLE) + } else { + wave_format.Format = *pwfx; + } + + audio_client.Start()?; + + Ok(Self { + audio_client, + render_client, + wave_format, + }) + } + } + + pub fn wave_format(&self) -> &WAVEFORMATEXTENSIBLE { + &self.wave_format + } + + pub fn render_frames(&self, data_in: *const u8, frames: u32) -> windows::core::Result<()> { + unsafe { + let padding = self.audio_client.GetCurrentPadding()?; + let frames = frames - padding; + + let data_out = self.render_client.GetBuffer(frames)?; + + let data_len = frames * self.wave_format.Format.nBlockAlign as u32; + + std::ptr::copy(data_in, data_out, data_len as usize); + + self.render_client.ReleaseBuffer(frames, 0)?; + } + + Ok(()) + } +} diff --git a/crates/nodio-win32/src/session.rs b/crates/nodio-win32/src/session.rs new file mode 100644 index 0000000..e5c7f84 --- /dev/null +++ b/crates/nodio-win32/src/session.rs @@ -0,0 +1,289 @@ +use std::mem::size_of_val; +use std::ptr::{null, null_mut}; +use std::sync::mpsc::channel; +use std::sync::Arc; +use std::time::Duration; + +use log::{trace, warn}; +use notify_thread::JoinHandle; +use parking_lot::Mutex; +use widestring::U16Str; +use windows::core::{Interface, PCWSTR, PWSTR}; +use windows::Win32::Foundation::{CloseHandle, BOOL, HINSTANCE}; +use windows::Win32::Media::Audio as windows_audio; +use windows::Win32::Media::Audio::Endpoints::IAudioMeterInformation; +use windows::Win32::Media::Audio::{ + AudioSessionState, IAudioSessionControl, IAudioSessionControl2, IAudioSessionEvents, + ISimpleAudioVolume, +}; +use windows::Win32::System::ProcessStatus::{ + K32EnumProcessModulesEx, K32GetModuleBaseNameW, K32GetModuleFileNameExW, LIST_MODULES_ALL, +}; +use windows::Win32::System::Threading::{OpenProcess, PROCESS_QUERY_INFORMATION, PROCESS_VM_READ}; +use windows::Win32::UI::Shell::SHLoadIndirectString; + +use nodio_core::{Node, NodeKind, Uuid}; + +use crate::custom::{AudioSessionEvent, AudioSessionEvents, SessionState}; +use crate::pwstr_to_string; +use crate::Callback; + +#[derive(Copy, Clone, Ord, PartialOrd, Eq, PartialEq)] +pub enum AudioSessionKind { + Application, + Other, +} + +#[derive(Clone)] +pub struct AudioSession { + id: Uuid, + process_id: u32, + display_name: String, + filename: String, + kind: AudioSessionKind, + control: IAudioSessionControl, + simple_audio_volume: ISimpleAudioVolume, + meter: IAudioMeterInformation, + events: IAudioSessionEvents, + event_thread_handle: Arc>>>, + event_callback: Arc>>>, +} + +impl Drop for AudioSession { + fn drop(&mut self) { + trace!("dropping audio session {}", self.display_name); + unsafe { + self.control + .UnregisterAudioSessionNotification(self.events.clone()) + .ok(); + } + if let Some(t) = self.event_thread_handle.lock().take() { + t.notify(); + } + trace!("audio session dropped"); + } +} + +unsafe impl Send for AudioSession {} +unsafe impl Sync for AudioSession {} + +impl AudioSession { + pub fn new(control: IAudioSessionControl) -> windows::core::Result { + let control2: IAudioSessionControl2 = control.cast()?; + let simple_audio_volume: ISimpleAudioVolume = control.cast()?; + let meter: IAudioMeterInformation = control.cast()?; + + let process_id = unsafe { control2.GetProcessId()? }; + let display_name_pwstr: PWSTR = unsafe { control.GetDisplayName()? }; + let mut display_name: String = pwstr_to_string(display_name_pwstr); + + if display_name.starts_with('@') { + let mut text = [0; 512]; + unsafe { + SHLoadIndirectString( + PCWSTR(display_name_pwstr.0 as *const u16), + &mut text, + null_mut(), + )? + }; + + let len = text.iter().take_while(|&&c| c != 0).count(); + + display_name = String::from_utf16_lossy(&text[..len]); + } + + if display_name.is_empty() { + display_name = get_process_name(process_id)?; + } + + let mut filename = String::new(); + if process_id != 0 { + let handle = unsafe { + OpenProcess( + PROCESS_QUERY_INFORMATION | PROCESS_VM_READ, + BOOL::from(false), + process_id, + ) + }?; + + let mut module_filename = [0; 2048]; + let nread = unsafe { + K32GetModuleFileNameExW(handle, HINSTANCE(0), module_filename.as_mut_slice()) + } as usize; + + filename = String::from_utf16_lossy(&module_filename[..nread]); + unsafe { CloseHandle(handle) }; + } + + let kind = if filename.is_empty() { + AudioSessionKind::Other + } else { + AudioSessionKind::Application + }; + + let (event_tx, event_rx) = channel(); + + let events = AudioSessionEvents::create(event_tx); + let session_event_callback: Arc>>> = + Arc::new(Mutex::new(None)); + let session_event_thread = { + let session_event_callback = session_event_callback.clone(); + + notify_thread::spawn(move |thread| loop { + match event_rx.recv_timeout(Duration::from_millis(100)) { + Ok(event) => { + trace!("Session event: {:?}", event); + + if let Some(cb) = session_event_callback.lock().as_ref() { + cb(event); + } + } + + _ if thread.notified() => { + trace!("Session event thread ended"); + return; + } + _ => {} + } + }) + }; + + unsafe { + control + .RegisterAudioSessionNotification(events.clone()) + .unwrap(); + }; + + Ok(Self { + id: Uuid::new_v4(), + process_id, + display_name, + filename, + kind, + control, + simple_audio_volume, + meter, + events, + event_thread_handle: Arc::new(Mutex::new(Some(session_event_thread))), + event_callback: session_event_callback, + }) + } + + pub fn set_event_callback(&mut self, cb: T) + where + T: Fn(AudioSessionEvent) + Send + Sync + 'static, + { + let _ = self.event_callback.lock().insert(Box::new(cb)); + } + + pub fn is_active(&self) -> bool { + let state: AudioSessionState = unsafe { self.control.GetState() }.unwrap(); + + state == windows_audio::AudioSessionStateActive + } + + pub fn set_master_volume(&self, volume: f32) { + unsafe { + if let Err(err) = self.simple_audio_volume.SetMasterVolume(volume, null()) { + warn!( + "Failed to set volume for session {}: {:?}", + self.process_id, err + ); + } + } + } + + pub fn master_volume(&self) -> f32 { + unsafe { self.simple_audio_volume.GetMasterVolume().unwrap_or(0.0) as f32 } + } + + pub fn _state(&self) -> SessionState { + match unsafe { self.control.GetState() }.unwrap_or(windows_audio::AudioSessionStateExpired) + { + windows_audio::AudioSessionStateActive => SessionState::Active, + windows_audio::AudioSessionStateInactive => SessionState::Inactive, + windows_audio::AudioSessionStateExpired => SessionState::Expired, + _ => SessionState::Expired, + } + } + + pub fn _muted(&self) -> bool { + unsafe { + self.simple_audio_volume + .GetMute() + .unwrap_or_default() + .into() + } + } + + pub fn peak_values(&self) -> windows::core::Result<(f32, f32)> { + unsafe { + let channel_count = usize::min(2, self.meter.GetMeteringChannelCount()? as usize); + + let mut values = [0.0; 2]; + self.meter.GetChannelsPeakValues(values.as_mut_slice())?; + + if channel_count == 1 { + Ok((values[0], values[0])) + } else { + Ok((values[0], values[1])) + } + } + } + + pub fn id(&self) -> Uuid { + self.id + } + + pub fn process_id(&self) -> u32 { + self.process_id + } + + pub fn display_name(&self) -> &str { + &self.display_name + } + + pub fn filename(&self) -> &str { + &self.filename + } + + pub fn kind(&self) -> AudioSessionKind { + self.kind + } +} + +pub fn session_node_match(node: &Node, session: &AudioSession) -> bool { + node.process_id == Some(session.process_id) + || (node.kind == NodeKind::Application + && node.display_name == session.display_name + && node.filename == session.filename + && session.kind == AudioSessionKind::Application) + || (node.kind == NodeKind::InputDevice + && node.display_name == session.display_name + && session.kind == AudioSessionKind::Other) +} + +fn get_process_name(pid: u32) -> windows::core::Result { + unsafe { + let proc = OpenProcess(PROCESS_QUERY_INFORMATION | PROCESS_VM_READ, false, pid)?; + + let mut hmodule = HINSTANCE::default(); + let mut bytes_needed = 0; + if K32EnumProcessModulesEx( + proc, + &mut hmodule, + size_of_val(&hmodule) as _, + &mut bytes_needed, + LIST_MODULES_ALL, + ) + .as_bool() + { + let mut name: [u16; 256] = [0; 256]; + let len = K32GetModuleBaseNameW(proc, hmodule, name.as_mut_slice()); + + Ok(U16Str::from_ptr(name.as_ptr(), len as _).to_string_lossy()) + } else { + Ok(String::default()) + } + } +} diff --git a/docs/screenshot.png b/docs/screenshot.png new file mode 100644 index 0000000..e6b4844 Binary files /dev/null and b/docs/screenshot.png differ diff --git a/src/main.rs b/src/main.rs new file mode 100644 index 0000000..f328e4d --- /dev/null +++ b/src/main.rs @@ -0,0 +1 @@ +fn main() {}