diff --git a/Cargo.lock b/Cargo.lock index 5be4d2e..b242a3d 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -11,24 +11,217 @@ dependencies = [ "memchr", ] +[[package]] +name = "allocator-api2" +version = "0.2.21" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "683d7910e743518b0e34f1186f92494becacb047c7b6bf616c96772180fef923" + +[[package]] +name = "android_system_properties" +version = "0.1.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "819e7219dbd41043ac279b19830f2efc897156490d7fd6ea916720117ee66311" +dependencies = [ + "libc", +] + +[[package]] +name = "arrayvec" +version = "0.7.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7c02d123df017efcdfbd739ef81735b36c5ba83ec3c59c80a9d7ecc718f92e50" + +[[package]] +name = "ash" +version = "0.38.0+1.3.281" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0bb44936d800fea8f016d7f2311c6a4f97aebd5dc86f09906139ec848cf3a46f" +dependencies = [ + "libloading", +] + [[package]] name = "atomic-waker" version = "1.1.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "1505bd5d3d116872e7271a6d4e16d81d0c8570876c8de68093a09ac269d8aac0" +[[package]] +name = "autocfg" +version = "1.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c08606f8c3cbf4ce6ec8e28fb0014a2c086708fe954eaa885384a6165172e7e8" + +[[package]] +name = "bit-set" +version = "0.9.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "34ddef2995421ab6a5c779542c81ee77c115206f4ad9d5a8e05f4ff49716a3dd" +dependencies = [ + "bit-vec", +] + +[[package]] +name = "bit-vec" +version = "0.9.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b71798fca2c1fe1086445a7258a4bc81e6e49dcd24c8d0dd9a1e57395b603f51" + +[[package]] +name = "bitflags" +version = "2.11.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "843867be96c8daad0d758b57df9392b6d8d271134fce549de6ce169ff98a92af" + +[[package]] +name = "block2" +version = "0.6.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cdeb9d870516001442e364c5220d3574d2da8dc765554b4a617230d33fa58ef5" +dependencies = [ + "objc2", +] + +[[package]] +name = "bumpalo" +version = "3.20.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5d20789868f4b01b2f2caec9f5c4e0213b41e3e5702a50157d699ae31ced2fcb" + +[[package]] +name = "bytemuck" +version = "1.25.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c8efb64bd706a16a1bdde310ae86b351e4d21550d98d056f22f8a7f7a2183fec" +dependencies = [ + "bytemuck_derive", +] + +[[package]] +name = "bytemuck_derive" +version = "1.10.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f9abbd1bc6865053c427f7198e6af43bfdedc55ab791faed4fbd361d789575ff" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + [[package]] name = "bytes" version = "1.11.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "1e748733b7cbc798e1434b6ac524f0c1ff2ab456fe201501e6497c8417a4fc33" +[[package]] +name = "cc" +version = "1.2.57" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7a0dd1ca384932ff3641c8718a02769f1698e7563dc6974ffd03346116310423" +dependencies = [ + "find-msvc-tools", + "shlex", +] + [[package]] name = "cfg-if" version = "1.0.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9330f8b2ff13f34540b44e946ef35111825727b38d33286ef986142615121801" +[[package]] +name = "cfg_aliases" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "613afe47fcd5fac7ccf1db93babcb082c5994d996f20b8b159f2ad1658eb5724" + +[[package]] +name = "codespan-reporting" +version = "0.13.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "af491d569909a7e4dee0ad7db7f5341fef5c614d5b8ec8cf765732aba3cff681" +dependencies = [ + "serde", + "termcolor", + "unicode-width", +] + +[[package]] +name = "crunchy" +version = "0.2.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "460fbee9c2c2f33933d720630a6a0bac33ba7053db5344fac858d4b8952d77d5" + +[[package]] +name = "dispatch2" +version = "0.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1e0e367e4e7da84520dedcac1901e4da967309406d1e51017ae1abfb97adbd38" +dependencies = [ + "bitflags", + "objc2", +] + +[[package]] +name = "dlib" +version = "0.5.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ab8ecd87370524b461f8557c119c405552c396ed91fc0a8eec68679eab26f94a" +dependencies = [ + "libloading", +] + +[[package]] +name = "document-features" +version = "0.2.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d4b8a88685455ed29a21542a33abd9cb6510b6b129abadabdcef0f4c55bc8f61" +dependencies = [ + "litrs", +] + +[[package]] +name = "downcast-rs" +version = "1.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "75b325c5dbd37f80359721ad39aca5a29fb04c89279657cffdda8736d0c0b9d2" + +[[package]] +name = "equivalent" +version = "1.0.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "877a4ace8713b0bcf2a4e7eec82529c029f1d0619886d18145fea96c3ffe5c0f" + +[[package]] +name = "errno" +version = "0.3.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "39cab71617ae0d63f51a36d69f866391735b51691dbda63cf6f96d042b63efeb" +dependencies = [ + "libc", + "windows-sys", +] + +[[package]] +name = "find-msvc-tools" +version = "0.1.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5baebc0774151f905a1a2cc41989300b1e6fbb29aff0ceffa1064fdd3088d582" + +[[package]] +name = "foldhash" +version = "0.1.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d9c4f5dac5e15c24eb999c26181a6ca40b39fe946cbe4c263c7209467bc83af2" + +[[package]] +name = "foldhash" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "77ce24cb58228fbb8aa041425bb1050850ac19177686ea6e0f41a70416f56fdb" + [[package]] name = "futures-channel" version = "0.3.32" @@ -44,6 +237,128 @@ version = "0.3.32" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "7e3450815272ef58cec6d564423f6e755e25379b217b0bc688e295ba24df6b1d" +[[package]] +name = "futures-task" +version = "0.3.32" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "037711b3d59c33004d3856fbdc83b99d4ff37a24768fa1be9ce3538a1cde4393" + +[[package]] +name = "futures-util" +version = "0.3.32" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "389ca41296e6190b48053de0321d02a77f32f8a5d2461dd38762c0593805c6d6" +dependencies = [ + "futures-core", + "futures-task", + "pin-project-lite", + "slab", +] + +[[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.17.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "29038e1c483364cc6bb3cf78feee1816002e127c331a1eec55a4d202b9e1adb5" +dependencies = [ + "js-sys", + "slotmap", + "wasm-bindgen", + "web-sys", +] + +[[package]] +name = "glutin_wgl_sys" +version = "0.6.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2c4ee00b289aba7a9e5306d57c2d05499b2e5dc427f84ac708bd2c090212cf3e" +dependencies = [ + "gl_generator", +] + +[[package]] +name = "gpu-allocator" +version = "0.28.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "51255ea7cfaadb6c5f1528d43e92a82acb2b96c43365989a28b2d44ee38f8795" +dependencies = [ + "ash", + "hashbrown 0.16.1", + "log", + "presser", + "thiserror", + "windows", +] + +[[package]] +name = "gpu-descriptor" +version = "0.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b89c83349105e3732062a895becfc71a8f921bb71ecbbdd8ff99263e3b53a0ca" +dependencies = [ + "bitflags", + "gpu-descriptor-types", + "hashbrown 0.15.5", +] + +[[package]] +name = "gpu-descriptor-types" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fdf242682df893b86f33a73828fb09ca4b2d3bb6cc95249707fc684d27484b91" +dependencies = [ + "bitflags", +] + +[[package]] +name = "half" +version = "2.7.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6ea2d84b969582b4b1864a92dc5d27cd2b77b622a8d79306834f1be5ba20d84b" +dependencies = [ + "cfg-if", + "crunchy", + "num-traits", + "zerocopy", +] + +[[package]] +name = "hashbrown" +version = "0.15.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9229cfe53dfd69f0609a49f65461bd93001ea1ef889cd5529dd176593f5338a1" +dependencies = [ + "foldhash 0.1.5", +] + +[[package]] +name = "hashbrown" +version = "0.16.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "841d1cc9bed7f9236f321df977030373f4a4163ae1a7dbfe1a51a2c1a51d9100" +dependencies = [ + "allocator-api2", + "equivalent", + "foldhash 0.2.0", +] + +[[package]] +name = "hexf-parse" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dfa686283ad6dd069f105e5ab091b04c62850d3e4cf5d67debad1933f55023df" + [[package]] name = "http" version = "1.4.0" @@ -104,12 +419,55 @@ dependencies = [ "want", ] +[[package]] +name = "indexmap" +version = "2.13.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7714e70437a7dc3ac8eb7e6f8df75fd8eb422675fc7678aff7364301092b1017" +dependencies = [ + "equivalent", + "hashbrown 0.16.1", +] + [[package]] name = "itoa" version = "1.0.17" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "92ecc6618181def0457392ccd0ee51198e065e016d1d527a7ac1b6dc7c1f09d2" +[[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.91" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b49715b7073f385ba4bc528e5747d02e66cb39c6146efb66b781f131f0fb399c" +dependencies = [ + "once_cell", + "wasm-bindgen", +] + +[[package]] +name = "khronos-egl" +version = "6.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6aae1df220ece3c0ada96b8153459b67eebe9ae9212258bb0134ae60416fdf76" +dependencies = [ + "libc", + "libloading", + "pkg-config", +] + +[[package]] +name = "khronos_api" +version = "3.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e2db585e1d738fc771bf08a151420d3ed193d9d895a36df7f6f8a9456b911ddc" + [[package]] name = "lazy_static" version = "1.5.0" @@ -122,6 +480,49 @@ version = "0.2.183" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b5b646652bf6661599e1da8901b3b9522896f01e736bad5f723fe7a3a27f899d" +[[package]] +name = "libloading" +version = "0.8.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d7c4b02199fee7c5d21a5ae7d8cfa79a6ef5bb2fc834d6e9058e89c825efdc55" +dependencies = [ + "cfg-if", + "windows-link", +] + +[[package]] +name = "libm" +version = "0.2.16" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b6d2cec3eae94f9f509c767b45932f1ada8350c4bdb85af2fcab4a3c14807981" + +[[package]] +name = "linux-raw-sys" +version = "0.12.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "32a66949e030da00e8c7d4434b251670a91556f4144941d37452769c25d58a53" + +[[package]] +name = "litrs" +version = "1.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "11d3d7f243d5c5a8b9bb5d6dd2b1602c0cb0b9db1621bafc7ed66e35ff9fe092" + +[[package]] +name = "lock_api" +version = "0.4.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "224399e74b87b5f3557511d98dff8b14089b3dadafcab6bb93eab67d3aace965" +dependencies = [ + "scopeguard", +] + +[[package]] +name = "log" +version = "0.4.29" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5e5032e24019045c762d3c0f28f5b6b8bbf38563a65908389bf7978758920897" + [[package]] name = "matchers" version = "0.2.0" @@ -137,12 +538,151 @@ version = "2.8.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "f8ca58f447f06ed17d5fc4043ce1b10dd205e060fb3ce5b979b8ed8e59ff3f79" +[[package]] +name = "naga" +version = "29.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "85b4372fed0bd362d646d01b6926df0e837859ccc522fed720c395e0460f29c8" +dependencies = [ + "arrayvec", + "bit-set", + "bitflags", + "cfg-if", + "cfg_aliases", + "codespan-reporting", + "half", + "hashbrown 0.16.1", + "hexf-parse", + "indexmap", + "libm", + "log", + "num-traits", + "once_cell", + "rustc-hash", + "spirv", + "thiserror", + "unicode-ident", +] + +[[package]] +name = "ndk-sys" +version = "0.6.0+11769913" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ee6cda3051665f1fb8d9e08fc35c96d5a244fb1be711a03b71118828afc9a873" +dependencies = [ + "jni-sys", +] + +[[package]] +name = "num-traits" +version = "0.2.19" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "071dfc062690e90b734c0b2273ce72ad0ffa95f0c74596bc250dcfd960262841" +dependencies = [ + "autocfg", + "libm", +] + +[[package]] +name = "objc2" +version = "0.6.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3a12a8ed07aefc768292f076dc3ac8c48f3781c8f2d5851dd3d98950e8c5a89f" +dependencies = [ + "objc2-encode", +] + +[[package]] +name = "objc2-core-foundation" +version = "0.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2a180dd8642fa45cdb7dd721cd4c11b1cadd4929ce112ebd8b9f5803cc79d536" +dependencies = [ + "bitflags", + "dispatch2", + "objc2", +] + +[[package]] +name = "objc2-encode" +version = "4.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ef25abbcd74fb2609453eb695bd2f860d389e457f67dc17cafc8b8cbc89d0c33" + +[[package]] +name = "objc2-foundation" +version = "0.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e3e0adef53c21f888deb4fa59fc59f7eb17404926ee8a6f59f5df0fd7f9f3272" +dependencies = [ + "bitflags", + "objc2", + "objc2-core-foundation", +] + +[[package]] +name = "objc2-metal" +version = "0.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a0125f776a10d00af4152d74616409f0d4a2053a6f57fa5b7d6aa2854ac04794" +dependencies = [ + "bitflags", + "block2", + "objc2", + "objc2-foundation", +] + +[[package]] +name = "objc2-quartz-core" +version = "0.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "96c1358452b371bf9f104e21ec536d37a650eb10f7ee379fff67d2e08d537f1f" +dependencies = [ + "bitflags", + "objc2", + "objc2-core-foundation", + "objc2-foundation", + "objc2-metal", +] + [[package]] name = "once_cell" version = "1.21.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9f7c3e4beb33f85d45ae3e3a1792185706c8e16d043238c593331cc7cd313b50" +[[package]] +name = "ordered-float" +version = "5.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7f4779c6901a562440c3786d08192c6fbda7c1c2060edd10006b05ee35d10f2d" +dependencies = [ + "num-traits", +] + +[[package]] +name = "parking_lot" +version = "0.12.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "93857453250e3077bd71ff98b6a65ea6621a19bb0f559a85248955ac12c45a1a" +dependencies = [ + "lock_api", + "parking_lot_core", +] + +[[package]] +name = "parking_lot_core" +version = "0.9.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2621685985a2ebf1c516881c026032ac7deafcda1a2c9b7850dc81e3dfcb64c1" +dependencies = [ + "cfg-if", + "libc", + "redox_syscall", + "smallvec", + "windows-link", +] + [[package]] name = "pin-project-lite" version = "0.2.17" @@ -155,6 +695,39 @@ version = "0.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "8b870d8c151b6f2fb93e84a13146138f05d02ed11c7e7c54f8826aaaf7c9f184" +[[package]] +name = "pkg-config" +version = "0.3.32" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7edddbd0b52d732b21ad9a5fab5c704c14cd949e5e9a1ec5929a24fded1b904c" + +[[package]] +name = "pollster" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2f3a9f18d041e6d0e102a0a46750538147e5e8992d3b4873aaafee2520b00ce3" + +[[package]] +name = "portable-atomic" +version = "1.13.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c33a9471896f1c69cecef8d20cbe2f7accd12527ce60845ff44c153bb2a21b49" + +[[package]] +name = "portable-atomic-util" +version = "0.2.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "091397be61a01d4be58e7841595bd4bfedb15f1cd54977d79b8271e94ed799a3" +dependencies = [ + "portable-atomic", +] + +[[package]] +name = "presser" +version = "0.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e8cf8e6a8aa66ce33f63993ffc4ea4271eb5b0530a9002db8455ea6050c77bfa" + [[package]] name = "proc-macro2" version = "1.0.106" @@ -164,6 +737,21 @@ dependencies = [ "unicode-ident", ] +[[package]] +name = "profiling" +version = "1.0.17" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3eb8486b569e12e2c32ad3e204dbaba5e4b5b216e9367044f25f1dba42341773" + +[[package]] +name = "quick-xml" +version = "0.39.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "958f21e8e7ceb5a1aa7fa87fab28e7c75976e0bfe7e23ff069e0a260f894067d" +dependencies = [ + "memchr", +] + [[package]] name = "quote" version = "1.0.45" @@ -173,6 +761,39 @@ dependencies = [ "proc-macro2", ] +[[package]] +name = "range-alloc" +version = "0.1.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ca45419789ae5a7899559e9512e58ca889e41f04f1f2445e9f4b290ceccd1d08" + +[[package]] +name = "raw-window-handle" +version = "0.6.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "20675572f6f24e9e76ef639bc5552774ed45f1c30e2951e1e99c59888861c539" + +[[package]] +name = "raw-window-metal" +version = "1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "40d213455a5f1dc59214213c7330e074ddf8114c9a42411eb890c767357ce135" +dependencies = [ + "objc2", + "objc2-core-foundation", + "objc2-foundation", + "objc2-quartz-core", +] + +[[package]] +name = "redox_syscall" +version = "0.5.18" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ed2bf2547551a7053d6fdfafda3f938979645c44812fbfcda098faae3f1a362d" +dependencies = [ + "bitflags", +] + [[package]] name = "regex-automata" version = "0.4.14" @@ -190,6 +811,12 @@ version = "0.8.10" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "dc897dd8d9e8bd1ed8cdad82b5966c3e0ecae09fb1907d58efaa013543185d0a" +[[package]] +name = "renderdoc-sys" +version = "1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "19b30a45b0cd0bcca8037f3d0dc3421eaf95327a17cad11964fb8179b4fc4832" + [[package]] name = "ruin-runtime" version = "0.1.0" @@ -220,6 +847,114 @@ dependencies = [ "tracing-subscriber", ] +[[package]] +name = "ruin_ui" +version = "0.1.0" +dependencies = [ + "ruin-runtime", + "ruin_reactivity", + "tracing", + "tracing-subscriber", +] + +[[package]] +name = "ruin_ui_platform_wayland" +version = "0.1.0" +dependencies = [ + "raw-window-handle", + "ruin_ui", + "wayland-backend", + "wayland-client", + "wayland-protocols", +] + +[[package]] +name = "ruin_ui_renderer_wgpu" +version = "0.1.0" +dependencies = [ + "bytemuck", + "pollster", + "raw-window-handle", + "ruin_ui", + "wgpu", +] + +[[package]] +name = "ruin_ui_wayland_demo" +version = "0.1.0" +dependencies = [ + "ruin_ui", + "ruin_ui_platform_wayland", + "ruin_ui_renderer_wgpu", +] + +[[package]] +name = "rustc-hash" +version = "1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "08d43f7aa6b08d49f382cde6a7982047c3426db949b1424bc4b7ec9ae12c6ce2" + +[[package]] +name = "rustix" +version = "1.1.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b6fe4565b9518b83ef4f91bb47ce29620ca828bd32cb7e408f0062e9930ba190" +dependencies = [ + "bitflags", + "errno", + "libc", + "linux-raw-sys", + "windows-sys", +] + +[[package]] +name = "rustversion" +version = "1.0.22" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b39cdef0fa800fc44525c84ccb54a029961a8215f9619753635a9c0d2538d46d" + +[[package]] +name = "scoped-tls" +version = "1.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e1cf6437eb19a8f4a6cc0f7dca544973b0b78843adbfeb3683d1a94a0024a294" + +[[package]] +name = "scopeguard" +version = "1.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "94143f37725109f92c262ed2cf5e59bce7498c01bcc1502d7b9afe439a4e9f49" + +[[package]] +name = "serde" +version = "1.0.228" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9a8e94ea7f378bd32cbbd37198a4a91436180c5bb472411e48b5ec2e2124ae9e" +dependencies = [ + "serde_core", + "serde_derive", +] + +[[package]] +name = "serde_core" +version = "1.0.228" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "41d385c7d4ca58e59fc732af25c3983b67ac852c1a25000afe1175de458b67ad" +dependencies = [ + "serde_derive", +] + +[[package]] +name = "serde_derive" +version = "1.0.228" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d540f220d3187173da220f885ab66608367b6574e925011a9353e4badda91d79" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + [[package]] name = "sharded-slab" version = "0.1.7" @@ -229,12 +964,48 @@ dependencies = [ "lazy_static", ] +[[package]] +name = "shlex" +version = "1.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0fda2ff0d084019ba4d7c6f371c95d8fd75ce3524c3cb8fb653a3023f6323e64" + +[[package]] +name = "slab" +version = "0.4.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0c790de23124f9ab44544d7ac05d60440adc586479ce501c1d6d7da3cd8c9cf5" + +[[package]] +name = "slotmap" +version = "1.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bdd58c3c93c3d278ca835519292445cb4b0d4dc59ccfdf7ceadaab3f8aeb4038" +dependencies = [ + "version_check", +] + [[package]] name = "smallvec" version = "1.15.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "67b1b7a3b5fe4f1376887184045fcf45c69e92af734b7aaddc05fb777b6fbd03" +[[package]] +name = "spirv" +version = "0.4.0+sdk-1.4.341.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d9571ea910ebd84c86af4b3ed27f9dbdc6ad06f17c5f96146b2b671e2976744f" +dependencies = [ + "bitflags", +] + +[[package]] +name = "static_assertions" +version = "1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a2eb9349b6444b326872e140eb1cf5e7c522154d69e7a0ffb0fb81c06b37543f" + [[package]] name = "syn" version = "2.0.117" @@ -246,6 +1017,35 @@ dependencies = [ "unicode-ident", ] +[[package]] +name = "termcolor" +version = "1.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "06794f8f6c5c898b3275aebefa6b8a1cb24cd2c6c79397ab15774837a0bc5755" +dependencies = [ + "winapi-util", +] + +[[package]] +name = "thiserror" +version = "2.0.18" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4288b5bcbc7920c07a1149a35cf9590a2aa808e0bc1eafaade0b80947865fbc4" +dependencies = [ + "thiserror-impl", +] + +[[package]] +name = "thiserror-impl" +version = "2.0.18" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ebc4ee7f67670e9b64d05fa4253e753e016c6c95ff35b89b7941d6b856dec1d5" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + [[package]] name = "thread_local" version = "1.1.9" @@ -310,6 +1110,18 @@ version = "1.0.24" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e6e4313cd5fcd3dad5cafa179702e2b244f760991f45397d14d4ebf38247da75" +[[package]] +name = "unicode-width" +version = "0.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b4ac048d71ede7ee76d585517add45da530660ef4390e49b098733c6e897f254" + +[[package]] +name = "version_check" +version = "0.9.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0b928f33d975fc6ad9f86c8f283853ad26bdd5b10b7f1542aa2fa15e2289105a" + [[package]] name = "want" version = "0.3.1" @@ -318,3 +1130,454 @@ checksum = "bfa7760aed19e106de2c7c0b581b509f2f25d3dacaf737cb82ac61bc6d760b0e" dependencies = [ "try-lock", ] + +[[package]] +name = "wasm-bindgen" +version = "0.2.114" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6532f9a5c1ece3798cb1c2cfdba640b9b3ba884f5db45973a6f442510a87d38e" +dependencies = [ + "cfg-if", + "once_cell", + "rustversion", + "wasm-bindgen-macro", + "wasm-bindgen-shared", +] + +[[package]] +name = "wasm-bindgen-futures" +version = "0.4.64" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e9c5522b3a28661442748e09d40924dfb9ca614b21c00d3fd135720e48b67db8" +dependencies = [ + "cfg-if", + "futures-util", + "js-sys", + "once_cell", + "wasm-bindgen", + "web-sys", +] + +[[package]] +name = "wasm-bindgen-macro" +version = "0.2.114" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "18a2d50fcf105fb33bb15f00e7a77b772945a2ee45dcf454961fd843e74c18e6" +dependencies = [ + "quote", + "wasm-bindgen-macro-support", +] + +[[package]] +name = "wasm-bindgen-macro-support" +version = "0.2.114" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "03ce4caeaac547cdf713d280eda22a730824dd11e6b8c3ca9e42247b25c631e3" +dependencies = [ + "bumpalo", + "proc-macro2", + "quote", + "syn", + "wasm-bindgen-shared", +] + +[[package]] +name = "wasm-bindgen-shared" +version = "0.2.114" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "75a326b8c223ee17883a4251907455a2431acc2791c98c26279376490c378c16" +dependencies = [ + "unicode-ident", +] + +[[package]] +name = "wayland-backend" +version = "0.3.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "aa75f400b7f719bcd68b3f47cd939ba654cedeef690f486db71331eec4c6a406" +dependencies = [ + "cc", + "downcast-rs", + "rustix", + "scoped-tls", + "smallvec", + "wayland-sys", +] + +[[package]] +name = "wayland-client" +version = "0.31.13" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ab51d9f7c071abeee76007e2b742499e535148035bb835f97aaed1338cf516c3" +dependencies = [ + "bitflags", + "rustix", + "wayland-backend", + "wayland-scanner", +] + +[[package]] +name = "wayland-protocols" +version = "0.32.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b23b5df31ceff1328f06ac607591d5ba360cf58f90c8fad4ac8d3a55a3c4aec7" +dependencies = [ + "bitflags", + "wayland-backend", + "wayland-client", + "wayland-scanner", +] + +[[package]] +name = "wayland-scanner" +version = "0.31.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c86287151a309799b821ca709b7345a048a2956af05957c89cb824ab919fa4e3" +dependencies = [ + "proc-macro2", + "quick-xml", + "quote", +] + +[[package]] +name = "wayland-sys" +version = "0.31.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "374f6b70e8e0d6bf9461a32988fd553b59ff630964924dad6e4a4eb6bd538d17" +dependencies = [ + "dlib", + "log", + "once_cell", + "pkg-config", +] + +[[package]] +name = "web-sys" +version = "0.3.91" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "854ba17bb104abfb26ba36da9729addc7ce7f06f5c0f90f3c391f8461cca21f9" +dependencies = [ + "js-sys", + "wasm-bindgen", +] + +[[package]] +name = "wgpu" +version = "29.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "78f9f386699b1fb8b8a05bfe82169b24d151f05702d2905a0bf93bc454fcc825" +dependencies = [ + "arrayvec", + "bitflags", + "bytemuck", + "cfg-if", + "cfg_aliases", + "document-features", + "hashbrown 0.16.1", + "js-sys", + "log", + "naga", + "parking_lot", + "portable-atomic", + "profiling", + "raw-window-handle", + "smallvec", + "static_assertions", + "wasm-bindgen", + "wasm-bindgen-futures", + "web-sys", + "wgpu-core", + "wgpu-hal", + "wgpu-types", +] + +[[package]] +name = "wgpu-core" +version = "29.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c7c34181b0acb8f98168f78f8e57ec66f57df5522b39143dbe5f2f45d7ca927c" +dependencies = [ + "arrayvec", + "bit-set", + "bit-vec", + "bitflags", + "bytemuck", + "cfg_aliases", + "document-features", + "hashbrown 0.16.1", + "indexmap", + "log", + "naga", + "once_cell", + "parking_lot", + "portable-atomic", + "profiling", + "raw-window-handle", + "rustc-hash", + "smallvec", + "thiserror", + "wgpu-core-deps-apple", + "wgpu-core-deps-emscripten", + "wgpu-core-deps-windows-linux-android", + "wgpu-hal", + "wgpu-naga-bridge", + "wgpu-types", +] + +[[package]] +name = "wgpu-core-deps-apple" +version = "29.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "43acd053312501689cd92a01a9638d37f3e41a5fd9534875efa8917ee2d11ac0" +dependencies = [ + "wgpu-hal", +] + +[[package]] +name = "wgpu-core-deps-emscripten" +version = "29.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ef043bf135cc68b6f667c55ff4e345ce2b5924d75bad36a47921b0287ca4b24a" +dependencies = [ + "wgpu-hal", +] + +[[package]] +name = "wgpu-core-deps-windows-linux-android" +version = "29.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "725d5c006a8c02967b6d93ef04f6537ec4593313e330cfe86d9d3f946eb90f28" +dependencies = [ + "wgpu-hal", +] + +[[package]] +name = "wgpu-hal" +version = "29.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "058b6047337cf323a4f092486443a9337f3d81325347e5d77deed7e563aeaedc" +dependencies = [ + "android_system_properties", + "arrayvec", + "ash", + "bit-set", + "bitflags", + "block2", + "bytemuck", + "cfg-if", + "cfg_aliases", + "glow", + "glutin_wgl_sys", + "gpu-allocator", + "gpu-descriptor", + "hashbrown 0.16.1", + "js-sys", + "khronos-egl", + "libc", + "libloading", + "log", + "naga", + "ndk-sys", + "objc2", + "objc2-core-foundation", + "objc2-foundation", + "objc2-metal", + "objc2-quartz-core", + "once_cell", + "ordered-float", + "parking_lot", + "portable-atomic", + "portable-atomic-util", + "profiling", + "range-alloc", + "raw-window-handle", + "raw-window-metal", + "renderdoc-sys", + "smallvec", + "thiserror", + "wasm-bindgen", + "wayland-sys", + "web-sys", + "wgpu-naga-bridge", + "wgpu-types", + "windows", + "windows-core", +] + +[[package]] +name = "wgpu-naga-bridge" +version = "29.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d0b8e1e505095f24cb4a578f04b1421d456257dca7fac114d9d9dd3d978c34b8" +dependencies = [ + "naga", + "wgpu-types", +] + +[[package]] +name = "wgpu-types" +version = "29.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d15ece45db77dd5451f11c0ce898334317ce8502d304a20454b531fdc0652fae" +dependencies = [ + "bitflags", + "bytemuck", + "js-sys", + "log", + "raw-window-handle", + "web-sys", +] + +[[package]] +name = "winapi-util" +version = "0.1.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c2a7b1c03c876122aa43f3020e6c3c3ee5c05081c9a00739faf7503aeba10d22" +dependencies = [ + "windows-sys", +] + +[[package]] +name = "windows" +version = "0.62.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "527fadee13e0c05939a6a05d5bd6eec6cd2e3dbd648b9f8e447c6518133d8580" +dependencies = [ + "windows-collections", + "windows-core", + "windows-future", + "windows-numerics", +] + +[[package]] +name = "windows-collections" +version = "0.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "23b2d95af1a8a14a3c7367e1ed4fc9c20e0a26e79551b1454d72583c97cc6610" +dependencies = [ + "windows-core", +] + +[[package]] +name = "windows-core" +version = "0.62.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b8e83a14d34d0623b51dce9581199302a221863196a1dde71a7663a4c2be9deb" +dependencies = [ + "windows-implement", + "windows-interface", + "windows-link", + "windows-result", + "windows-strings", +] + +[[package]] +name = "windows-future" +version = "0.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e1d6f90251fe18a279739e78025bd6ddc52a7e22f921070ccdc67dde84c605cb" +dependencies = [ + "windows-core", + "windows-link", + "windows-threading", +] + +[[package]] +name = "windows-implement" +version = "0.60.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "053e2e040ab57b9dc951b72c264860db7eb3b0200ba345b4e4c3b14f67855ddf" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "windows-interface" +version = "0.59.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3f316c4a2570ba26bbec722032c4099d8c8bc095efccdc15688708623367e358" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "windows-link" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f0805222e57f7521d6a62e36fa9163bc891acd422f971defe97d64e70d0a4fe5" + +[[package]] +name = "windows-numerics" +version = "0.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6e2e40844ac143cdb44aead537bbf727de9b044e107a0f1220392177d15b0f26" +dependencies = [ + "windows-core", + "windows-link", +] + +[[package]] +name = "windows-result" +version = "0.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7781fa89eaf60850ac3d2da7af8e5242a5ea78d1a11c49bf2910bb5a73853eb5" +dependencies = [ + "windows-link", +] + +[[package]] +name = "windows-strings" +version = "0.5.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7837d08f69c77cf6b07689544538e017c1bfcf57e34b4c0ff58e6c2cd3b37091" +dependencies = [ + "windows-link", +] + +[[package]] +name = "windows-sys" +version = "0.61.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ae137229bcbd6cdf0f7b80a31df61766145077ddf49416a728b02cb3921ff3fc" +dependencies = [ + "windows-link", +] + +[[package]] +name = "windows-threading" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3949bd5b99cafdf1c7ca86b43ca564028dfe27d66958f2470940f73d86d75b37" +dependencies = [ + "windows-link", +] + +[[package]] +name = "xml-rs" +version = "0.8.28" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3ae8337f8a065cfc972643663ea4279e04e7256de865aa66fe25cec5fb912d3f" + +[[package]] +name = "zerocopy" +version = "0.8.47" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "efbb2a062be311f2ba113ce66f697a4dc589f85e78a4aea276200804cea0ed87" +dependencies = [ + "zerocopy-derive", +] + +[[package]] +name = "zerocopy-derive" +version = "0.8.47" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0e8bc7269b54418e7aeeef514aa68f8690b8c0489a06b0136e5f57c4c5ccab89" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] diff --git a/Cargo.toml b/Cargo.toml index 54eefb2..b566048 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,3 +1,3 @@ [workspace] resolver = "3" -members = ["lib/*"] +members = ["lib/*", "examples/*"] diff --git a/examples/wayland_wgpu_demo/Cargo.toml b/examples/wayland_wgpu_demo/Cargo.toml new file mode 100644 index 0000000..206246d --- /dev/null +++ b/examples/wayland_wgpu_demo/Cargo.toml @@ -0,0 +1,9 @@ +[package] +name = "ruin_ui_wayland_demo" +version = "0.1.0" +edition = "2024" + +[dependencies] +ruin_ui = { path = "../../lib/ui" } +ruin_ui_platform_wayland = { path = "../../lib/ui_platform_wayland" } +ruin_ui_renderer_wgpu = { path = "../../lib/ui_renderer_wgpu" } diff --git a/examples/wayland_wgpu_demo/src/main.rs b/examples/wayland_wgpu_demo/src/main.rs new file mode 100644 index 0000000..1185ce0 --- /dev/null +++ b/examples/wayland_wgpu_demo/src/main.rs @@ -0,0 +1,62 @@ +use std::error::Error; + +use ruin_ui::{Color, Rect, SceneSnapshot, UiSize, WindowSpec}; +use ruin_ui_platform_wayland::WaylandWindow; +use ruin_ui_renderer_wgpu::{RenderError, WgpuSceneRenderer}; + +fn build_demo_scene() -> SceneSnapshot { + let mut scene = SceneSnapshot::new(1, UiSize::new(800.0, 500.0)); + scene.push_quad( + Rect::new(0.0, 0.0, 800.0, 500.0), + Color::rgb(0x10, 0x14, 0x24), + ); + scene.push_quad( + Rect::new(32.0, 32.0, 736.0, 72.0), + Color::rgb(0x2D, 0x3E, 0x68), + ); + scene.push_quad( + Rect::new(32.0, 136.0, 352.0, 300.0), + Color::rgb(0x6A, 0x3D, 0x3D), + ); + scene.push_quad( + Rect::new(416.0, 136.0, 352.0, 300.0), + Color::rgb(0x3A, 0x5E, 0x49), + ); + scene +} + +fn main() -> Result<(), Box> { + let scene = build_demo_scene(); + let mut window = WaylandWindow::open( + WindowSpec::new("RUIN Wayland / wgpu demo") + .app_id("dev.ruin.prototype") + .requested_inner_size(scene.logical_size), + )?; + let mut renderer = WgpuSceneRenderer::new( + window.surface_target(), + scene.logical_size.width as u32, + scene.logical_size.height as u32, + )?; + + println!("Opening RUIN Wayland / wgpu demo window..."); + while window.is_running() { + window.dispatch()?; + if let Some(frame) = window.prepare_frame() { + if frame.resized { + renderer.resize(frame.width, frame.height); + } + match renderer.render(&scene) { + Ok(()) => {} + Err(RenderError::Lost | RenderError::Outdated) => { + renderer.resize(frame.width, frame.height); + window.request_redraw(); + } + Err(RenderError::Timeout | RenderError::Occluded | RenderError::Validation) => { + window.request_redraw(); + } + } + } + } + + Ok(()) +} diff --git a/lib/ui/Cargo.toml b/lib/ui/Cargo.toml new file mode 100644 index 0000000..5179c83 --- /dev/null +++ b/lib/ui/Cargo.toml @@ -0,0 +1,12 @@ +[package] +name = "ruin_ui" +version = "0.1.0" +edition = "2024" + +[dependencies] +ruin_reactivity = { path = "../reactivity" } +ruin_runtime = { package = "ruin-runtime", path = "../runtime" } +tracing = { version = "0.1", default-features = false, features = ["std"] } + +[dev-dependencies] +tracing-subscriber = { version = "0.3", default-features = false, features = ["env-filter", "fmt", "std"] } diff --git a/lib/ui/examples/explicit_ui_prototype.rs b/lib/ui/examples/explicit_ui_prototype.rs new file mode 100644 index 0000000..3e894bb --- /dev/null +++ b/lib/ui/examples/explicit_ui_prototype.rs @@ -0,0 +1,247 @@ +use ruin_reactivity::{Cell, cell}; +use ruin_ui::{ + Color, PlatformEvent, Point, PreparedText, Rect, SceneSnapshot, UiRuntime, UiSize, + WindowController, WindowSpec, WindowUpdate, +}; +use tracing_subscriber::layer::SubscriberExt; +use tracing_subscriber::util::SubscriberInitExt; +use tracing_subscriber::{EnvFilter, fmt}; + +fn install_tracing() { + let filter = EnvFilter::try_from_default_env().unwrap_or_else(|_| { + EnvFilter::new( + "info,ruin_runtime::runtime=debug,ruin_ui::platform=debug,ruin_reactivity::effect=debug", + ) + }); + + let fmt_layer = fmt::layer() + .with_target(true) + .with_thread_ids(true) + .with_thread_names(true) + .compact(); + + let _ = tracing_subscriber::registry() + .with(filter) + .with(fmt_layer) + .try_init(); +} + +fn log_platform_event(event: &PlatformEvent) { + match event { + PlatformEvent::Opened { window_id } => { + tracing::info!(event = "window_opened", window_id = window_id.raw()); + } + PlatformEvent::Configured { + window_id, + configuration, + } => { + tracing::info!( + event = "window_configured", + window_id = window_id.raw(), + width = configuration.actual_inner_size.width, + height = configuration.actual_inner_size.height, + visible = configuration.visible, + "window configured" + ); + } + PlatformEvent::VisibilityChanged { window_id, visible } => { + tracing::info!( + event = "visibility_changed", + window_id = window_id.raw(), + visible, + "window visibility changed" + ); + } + PlatformEvent::FramePresented { + window_id, + scene_version, + item_count, + } => { + tracing::info!( + event = "frame_presented", + window_id = window_id.raw(), + scene_version, + item_count, + "headless backend presented scene" + ); + } + PlatformEvent::Closed { window_id } => { + tracing::info!(event = "window_closed", window_id = window_id.raw()); + } + PlatformEvent::CloseRequested { window_id } => { + tracing::info!( + event = "close_requested", + window_id = window_id.raw(), + "compositor-like close request received" + ); + } + } +} + +fn attach_window_scene(window: &WindowController) -> (Cell, ruin_reactivity::EffectHandle) { + let counter = cell(0usize); + let scene_window = window.clone(); + let scene_effect = scene_window.attach_scene_effect({ + let counter = counter.clone(); + move || { + let version = counter.get() as u64 + 1; + let value = counter.get(); + let mut scene = SceneSnapshot::new(version, UiSize::new(640.0, 360.0)); + scene + .push_quad( + Rect::new(0.0, 0.0, 640.0, 360.0), + Color::rgb(0x12, 0x19, 0x28), + ) + .push_quad( + Rect::new(24.0, 24.0, 592.0, 64.0), + Color::rgb(0x2B, 0x3A, 0x67), + ) + .push_text(PreparedText::monospace( + format!("RUIN explicit prototype | counter = {value}"), + Point::new(40.0, 64.0), + 18.0, + 9.0, + Color::rgb(0xFF, 0xFF, 0xFF), + )); + scene + } + }); + + (counter, scene_effect) +} + +#[ruin_runtime::async_main] +async fn main() -> Result<(), Box> { + install_tracing(); + + tracing::info!( + event = "prototype_start", + "starting explicit-construction UI prototype with a headless backend" + ); + + let mut ui = UiRuntime::headless(); + let window = ui.create_window( + WindowSpec::new("RUIN Explicit Prototype") + .app_id("dev.ruin.prototype") + .requested_inner_size(UiSize::new(640.0, 360.0)), + )?; + let (counter, _scene_effect) = attach_window_scene(&window); + let _ = counter.set(1); + let _ = counter.set(2); + let _ = counter.set(3); + + let event = ui + .wait_for_event_matching(|event| { + matches!( + event, + PlatformEvent::Configured { window_id, .. } if *window_id == window.id() + ) + }) + .await?; + log_platform_event(&event); + + let event = ui + .wait_for_event_matching( + |event| matches!(event, PlatformEvent::Opened { window_id } if *window_id == window.id()), + ) + .await?; + log_platform_event(&event); + + let event = ui + .wait_for_event_matching(|event| { + matches!( + event, + PlatformEvent::FramePresented { + window_id, + scene_version: 4, + .. + } if *window_id == window.id() + ) + }) + .await?; + log_platform_event(&event); + + window.update(WindowUpdate::new().visible(false))?; + let event = ui + .wait_for_event_matching(|event| { + matches!( + event, + PlatformEvent::VisibilityChanged { + window_id, + visible: false, + } if *window_id == window.id() + ) + }) + .await?; + log_platform_event(&event); + + window.update(WindowUpdate::new().visible(true))?; + let event = ui + .wait_for_event_matching(|event| { + matches!( + event, + PlatformEvent::VisibilityChanged { + window_id, + visible: true, + } if *window_id == window.id() + ) + }) + .await?; + log_platform_event(&event); + + let _ = counter.set(4); + let event = ui + .wait_for_event_matching(|event| { + matches!( + event, + PlatformEvent::FramePresented { + window_id, + scene_version: 4, + .. + } if *window_id == window.id() + ) + }) + .await?; + log_platform_event(&event); + + let event = ui + .wait_for_event_matching(|event| { + matches!( + event, + PlatformEvent::FramePresented { + window_id, + scene_version: 5, + .. + } if *window_id == window.id() + ) + }) + .await?; + log_platform_event(&event); + + window.emit_close_requested()?; + let event = ui + .wait_for_event_matching(|event| { + matches!( + event, + PlatformEvent::CloseRequested { window_id } if *window_id == window.id() + ) + }) + .await?; + log_platform_event(&event); + + window.update(WindowUpdate::new().open(false))?; + let event = ui + .wait_for_event_matching( + |event| matches!(event, PlatformEvent::Closed { window_id } if *window_id == window.id()), + ) + .await?; + log_platform_event(&event); + + ui.shutdown()?; + + tracing::info!( + event = "prototype_done", + "explicit-construction UI prototype finished" + ); + Ok(()) +} diff --git a/lib/ui/src/lib.rs b/lib/ui/src/lib.rs new file mode 100644 index 0000000..a2c999d --- /dev/null +++ b/lib/ui/src/lib.rs @@ -0,0 +1,26 @@ +//! Shared explicit-construction UI substrate for RUIN. +//! +//! This crate intentionally starts with explicit Rust data construction rather than a proc-macro +//! authoring layer. The goal is to validate the threading, windowing, and scene handoff model +//! before committing to ergonomic surface syntax. Concrete platform and renderer backends live in +//! sibling crates. + +pub(crate) mod trace_targets { + pub const PLATFORM: &str = "ruin_ui::platform"; + pub const SCENE: &str = "ruin_ui::scene"; +} + +mod platform; +mod runtime; +mod scene; +mod window; + +pub use platform::{PlatformClosed, PlatformEvent, PlatformProxy, PlatformRuntime, start_headless}; +pub use runtime::{EventStreamClosed, UiRuntime, WindowController}; +pub use scene::{ + Color, DisplayItem, GlyphInstance, Point, PreparedText, Quad, Rect, SceneSnapshot, Translation, + UiSize, +}; +pub use window::{ + DecorationMode, WindowConfigured, WindowId, WindowLifecycle, WindowSpec, WindowUpdate, +}; diff --git a/lib/ui/src/platform.rs b/lib/ui/src/platform.rs new file mode 100644 index 0000000..e67d8eb --- /dev/null +++ b/lib/ui/src/platform.rs @@ -0,0 +1,647 @@ +//! Headless platform/render worker for the explicit-construction prototype. + +use std::cell::RefCell; +use std::collections::BTreeMap; +use std::fmt; +use std::rc::Rc; +use std::sync::Arc; +use std::sync::atomic::{AtomicU64, Ordering}; + +use ruin_runtime::channel::mpsc; +use ruin_runtime::{WorkerHandle, queue_future, queue_microtask, spawn_worker}; +use tracing::{debug, info}; + +use crate::scene::{SceneSnapshot, UiSize}; +use crate::trace_targets; +use crate::window::{WindowConfigured, WindowId, WindowLifecycle, WindowSpec, WindowUpdate}; + +#[derive(Clone)] +pub struct PlatformProxy { + command_tx: mpsc::UnboundedSender, + next_window_id: Arc, +} + +/// Low-level platform/runtime host for the explicit prototype. +/// +/// The headless backend currently co-locates logical platform and rendering work on one runtime +/// worker thread, but the event/snapshot API is intentionally shaped so future backends can split +/// them if needed. +pub struct PlatformRuntime { + proxy: PlatformProxy, + events: mpsc::Receiver, + _worker: WorkerHandle, +} + +#[derive(Clone, Debug, PartialEq)] +pub enum PlatformEvent { + Opened { + window_id: WindowId, + }, + Closed { + window_id: WindowId, + }, + VisibilityChanged { + window_id: WindowId, + visible: bool, + }, + Configured { + window_id: WindowId, + configuration: WindowConfigured, + }, + FramePresented { + window_id: WindowId, + scene_version: u64, + item_count: usize, + }, + CloseRequested { + window_id: WindowId, + }, +} + +#[derive(Clone, Copy, Debug, Eq, PartialEq)] +pub struct PlatformClosed; + +impl fmt::Display for PlatformClosed { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + f.write_str("platform worker is closed") + } +} + +impl std::error::Error for PlatformClosed {} + +#[derive(Clone, Debug)] +enum PlatformCommand { + CreateWindow { + window_id: WindowId, + spec: WindowSpec, + }, + UpdateWindow { + window_id: WindowId, + update: WindowUpdate, + }, + ReplaceScene { + window_id: WindowId, + scene: SceneSnapshot, + }, + EmitCloseRequested { + window_id: WindowId, + }, + Shutdown, +} + +#[derive(Clone)] +struct HeadlessState { + events: mpsc::UnboundedSender, + windows: BTreeMap, +} + +#[derive(Clone)] +struct HeadlessWindow { + spec: WindowSpec, + lifecycle: WindowLifecycle, + configuration: WindowConfigured, + pending_scene: Option, + render_scheduled: bool, +} + +impl HeadlessWindow { + fn new(spec: WindowSpec) -> Self { + let actual_inner_size = spec + .requested_inner_size + .unwrap_or_else(|| UiSize::new(800.0, 600.0)); + Self { + spec, + lifecycle: WindowLifecycle::LogicalClosed, + configuration: WindowConfigured { + actual_inner_size, + scale_factor: 1.0, + visible: false, + maximized: false, + fullscreen: false, + }, + pending_scene: None, + render_scheduled: false, + } + } +} + +impl PlatformRuntime { + pub fn headless() -> Self { + start_headless() + } + + pub fn proxy(&self) -> PlatformProxy { + self.proxy.clone() + } + + pub async fn next_event(&mut self) -> Option { + self.events.recv().await + } +} + +impl PlatformProxy { + pub fn create_window(&self, spec: WindowSpec) -> Result { + let window_id = WindowId::from_raw(self.next_window_id.fetch_add(1, Ordering::Relaxed)); + self.send(PlatformCommand::CreateWindow { window_id, spec })?; + Ok(window_id) + } + + pub fn update_window( + &self, + window_id: WindowId, + update: WindowUpdate, + ) -> Result<(), PlatformClosed> { + self.send(PlatformCommand::UpdateWindow { window_id, update }) + } + + pub fn replace_scene( + &self, + window_id: WindowId, + scene: SceneSnapshot, + ) -> Result<(), PlatformClosed> { + self.send(PlatformCommand::ReplaceScene { window_id, scene }) + } + + pub fn emit_close_requested(&self, window_id: WindowId) -> Result<(), PlatformClosed> { + self.send(PlatformCommand::EmitCloseRequested { window_id }) + } + + pub fn shutdown(&self) -> Result<(), PlatformClosed> { + self.send(PlatformCommand::Shutdown) + } + + fn send(&self, command: PlatformCommand) -> Result<(), PlatformClosed> { + self.command_tx.send(command).map_err(|_| PlatformClosed) + } +} + +/// Starts the headless prototype backend. +/// +/// Policy note: when a window becomes visible again, the backend may re-present the latest retained +/// scene for that window even if the scene version itself did not change. Visibility restoration is +/// treated as a presentation-affecting state change. +pub fn start_headless() -> PlatformRuntime { + let (command_tx, mut command_rx) = mpsc::unbounded_channel::(); + let (event_tx, event_rx) = mpsc::unbounded_channel::(); + + let worker = spawn_worker( + move || { + let state = Rc::new(RefCell::new(HeadlessState { + events: event_tx.clone(), + windows: BTreeMap::new(), + })); + + queue_future(async move { + debug!( + target: trace_targets::PLATFORM, + event = "worker_started", + backend = "headless", + "starting headless platform worker" + ); + + while let Some(command) = command_rx.recv().await { + match command { + PlatformCommand::CreateWindow { window_id, spec } => { + handle_create_window(&state, window_id, spec); + } + PlatformCommand::UpdateWindow { window_id, update } => { + handle_update_window(&state, window_id, update); + } + PlatformCommand::ReplaceScene { window_id, scene } => { + handle_replace_scene(&state, window_id, scene); + } + PlatformCommand::EmitCloseRequested { window_id } => { + let sender = state.borrow().events.clone(); + let _ = sender.send(PlatformEvent::CloseRequested { window_id }); + } + PlatformCommand::Shutdown => { + debug!( + target: trace_targets::PLATFORM, + event = "worker_shutdown_requested", + "shutting down headless platform worker" + ); + break; + } + } + } + }); + }, + || { + debug!( + target: trace_targets::PLATFORM, + event = "worker_exited", + backend = "headless", + "headless platform worker exited" + ); + }, + ); + + PlatformRuntime { + proxy: PlatformProxy { + command_tx, + next_window_id: Arc::new(AtomicU64::new(1)), + }, + events: event_rx, + _worker: worker, + } +} + +fn handle_create_window(state: &Rc>, window_id: WindowId, spec: WindowSpec) { + debug!( + target: trace_targets::PLATFORM, + event = "create_window", + window_id = window_id.raw(), + title = spec.title.as_str(), + open = spec.open, + visible = spec.visible, + "creating logical window" + ); + + state + .borrow_mut() + .windows + .insert(window_id, HeadlessWindow::new(spec.clone())); + + if spec.open { + begin_open_transition(Rc::clone(state), window_id); + } +} + +fn handle_update_window( + state: &Rc>, + window_id: WindowId, + update: WindowUpdate, +) { + let mut action = UpdateAction::None; + { + let mut state_ref = state.borrow_mut(); + let Some(window) = state_ref.windows.get_mut(&window_id) else { + return; + }; + let was_open = window.spec.open; + let was_visible = window.spec.visible; + update.apply_to(&mut window.spec); + + if let Some(requested_inner_size) = window.spec.requested_inner_size { + window.configuration.actual_inner_size = requested_inner_size; + } + window.configuration.maximized = window.spec.maximized; + window.configuration.fullscreen = window.spec.fullscreen; + + match (was_open, window.spec.open) { + (false, true) => action = UpdateAction::Open, + (true, false) => action = UpdateAction::Close, + _ => {} + } + + if was_open == window.spec.open && was_visible != window.spec.visible { + action = UpdateAction::Visibility(window.spec.visible); + } + } + + match action { + UpdateAction::None => { + maybe_schedule_render(Rc::clone(state), window_id); + } + UpdateAction::Open => begin_open_transition(Rc::clone(state), window_id), + UpdateAction::Close => close_window(state, window_id), + UpdateAction::Visibility(visible) => set_visibility(state, window_id, visible), + } +} + +fn handle_replace_scene( + state: &Rc>, + window_id: WindowId, + scene: SceneSnapshot, +) { + #[cfg(debug_assertions)] + tracing::trace!( + target: trace_targets::PLATFORM, + event = "replace_scene", + window_id = window_id.raw(), + version = scene.version, + items = scene.item_count(), + "received scene snapshot" + ); + + if let Some(window) = state.borrow_mut().windows.get_mut(&window_id) { + window.pending_scene = Some(scene); + } else { + return; + } + + maybe_schedule_render(Rc::clone(state), window_id); +} + +fn maybe_schedule_render(state: Rc>, window_id: WindowId) { + let should_schedule = { + let mut state_ref = state.borrow_mut(); + let Some(window) = state_ref.windows.get_mut(&window_id) else { + return; + }; + if window.render_scheduled { + false + } else { + window.render_scheduled = true; + true + } + }; + + if should_schedule { + queue_microtask(move || present_latest_scene(&state, window_id)); + } +} + +fn begin_open_transition(state: Rc>, window_id: WindowId) { + { + let mut state_ref = state.borrow_mut(); + let Some(window) = state_ref.windows.get_mut(&window_id) else { + return; + }; + window.lifecycle = WindowLifecycle::Opening; + } + + queue_microtask(move || { + { + let mut state_ref = state.borrow_mut(); + let Some(window) = state_ref.windows.get_mut(&window_id) else { + return; + }; + window.lifecycle = WindowLifecycle::AwaitingInitialConfigure; + window.configuration.visible = window.spec.visible; + window.configuration.maximized = window.spec.maximized; + window.configuration.fullscreen = window.spec.fullscreen; + window.lifecycle = if window.spec.visible { + WindowLifecycle::OpenVisible + } else { + WindowLifecycle::OpenHidden + }; + } + + emit_event( + &state, + PlatformEvent::Configured { + window_id, + configuration: state + .borrow() + .windows + .get(&window_id) + .expect("window should exist during configure") + .configuration, + }, + ); + emit_event(&state, PlatformEvent::Opened { window_id }); + maybe_schedule_render(state, window_id); + }); +} + +fn close_window(state: &Rc>, window_id: WindowId) { + let should_emit = { + let mut state_ref = state.borrow_mut(); + let Some(window) = state_ref.windows.get_mut(&window_id) else { + return; + }; + if window.lifecycle == WindowLifecycle::LogicalClosed { + false + } else { + window.lifecycle = WindowLifecycle::Closing; + window.render_scheduled = false; + window.configuration.visible = false; + window.lifecycle = WindowLifecycle::LogicalClosed; + true + } + }; + + if should_emit { + emit_event(state, PlatformEvent::Closed { window_id }); + } +} + +fn set_visibility(state: &Rc>, window_id: WindowId, visible: bool) { + let should_emit = { + let mut state_ref = state.borrow_mut(); + let Some(window) = state_ref.windows.get_mut(&window_id) else { + return; + }; + if !window.spec.open { + false + } else { + window.configuration.visible = visible; + window.lifecycle = if visible { + WindowLifecycle::OpenVisible + } else { + WindowLifecycle::OpenHidden + }; + true + } + }; + + if should_emit { + emit_event( + state, + PlatformEvent::VisibilityChanged { window_id, visible }, + ); + maybe_schedule_render(Rc::clone(state), window_id); + } +} + +fn present_latest_scene(state: &Rc>, window_id: WindowId) { + let presentation = { + let mut state_ref = state.borrow_mut(); + let Some(window) = state_ref.windows.get_mut(&window_id) else { + return; + }; + window.render_scheduled = false; + if window.lifecycle != WindowLifecycle::OpenVisible { + return; + } + let Some(scene) = window.pending_scene.clone() else { + return; + }; + Some((scene.version, scene.item_count(), window.spec.title.clone())) + }; + + let Some((scene_version, item_count, title)) = presentation else { + return; + }; + + info!( + target: trace_targets::PLATFORM, + event = "present_scene", + backend = "headless", + window_id = window_id.raw(), + title = title.as_str(), + scene_version, + item_count, + "presenting latest scene snapshot" + ); + emit_event( + state, + PlatformEvent::FramePresented { + window_id, + scene_version, + item_count, + }, + ); +} + +fn emit_event(state: &Rc>, event: PlatformEvent) { + let sender = state.borrow().events.clone(); + let _ = sender.send(event); +} + +enum UpdateAction { + None, + Open, + Close, + Visibility(bool), +} + +#[cfg(test)] +mod tests { + use super::{PlatformEvent, start_headless}; + use crate::scene::{Color, Point, PreparedText, Rect, SceneSnapshot, UiSize}; + use crate::window::{WindowSpec, WindowUpdate}; + use ruin_runtime::{current_thread_handle, queue_future, run}; + use std::sync::{Arc, Mutex}; + + #[test] + fn headless_platform_coalesces_latest_scene_per_window() { + let observed = Arc::new(Mutex::new(Vec::::new())); + let done = Arc::new(Mutex::new(false)); + let main = current_thread_handle(); + + queue_future({ + let observed = Arc::clone(&observed); + let done = Arc::clone(&done); + async move { + let mut platform = start_headless(); + let proxy = platform.proxy(); + let window_id = proxy + .create_window( + WindowSpec::new("coalesce").requested_inner_size(UiSize::new(320.0, 180.0)), + ) + .expect("window should be created"); + + let mut one = SceneSnapshot::new(1, UiSize::new(320.0, 180.0)); + one.push_quad( + Rect::new(0.0, 0.0, 320.0, 180.0), + Color::rgb(0x22, 0x22, 0x33), + ); + let mut two = SceneSnapshot::new(2, UiSize::new(320.0, 180.0)); + two.push_quad( + Rect::new(0.0, 0.0, 320.0, 180.0), + Color::rgb(0x33, 0x22, 0x22), + ); + let mut three = SceneSnapshot::new(3, UiSize::new(320.0, 180.0)); + three + .push_quad( + Rect::new(0.0, 0.0, 320.0, 180.0), + Color::rgb(0x22, 0x33, 0x22), + ) + .push_text(PreparedText::monospace( + "v3", + Point::new(16.0, 28.0), + 16.0, + 8.0, + Color::rgb(0xFF, 0xFF, 0xFF), + )); + + proxy + .replace_scene(window_id, one) + .expect("scene v1 should queue"); + proxy + .replace_scene(window_id, two) + .expect("scene v2 should queue"); + proxy + .replace_scene(window_id, three) + .expect("scene v3 should queue"); + + while let Some(event) = platform.next_event().await { + observed.lock().unwrap().push(event.clone()); + if let PlatformEvent::FramePresented { + window_id: presented, + scene_version, + .. + } = event + { + assert_eq!(presented, window_id); + assert_eq!(scene_version, 3); + proxy.shutdown().expect("shutdown should queue"); + break; + } + } + + while platform.next_event().await.is_some() {} + *done.lock().unwrap() = true; + let _ = main.queue_task(|| {}); + } + }); + + run(); + assert!(*done.lock().unwrap()); + } + + #[test] + fn open_false_closes_and_reopen_reconfigures() { + let counts = Arc::new(Mutex::new((0usize, 0usize, 0usize))); + let done = Arc::new(Mutex::new(false)); + let main = current_thread_handle(); + + queue_future({ + let counts = Arc::clone(&counts); + let done = Arc::clone(&done); + async move { + let mut platform = start_headless(); + let proxy = platform.proxy(); + let window_id = proxy + .create_window( + WindowSpec::new("reopen").requested_inner_size(UiSize::new(640.0, 360.0)), + ) + .expect("window should be created"); + + let mut saw_initial_open = false; + while let Some(event) = platform.next_event().await { + match event { + PlatformEvent::Configured { window_id: id, .. } if id == window_id => { + counts.lock().unwrap().0 += 1; + } + PlatformEvent::Opened { window_id: id } if id == window_id => { + counts.lock().unwrap().1 += 1; + if !saw_initial_open { + saw_initial_open = true; + proxy + .update_window(window_id, WindowUpdate::new().open(false)) + .expect("close should queue"); + proxy + .update_window(window_id, WindowUpdate::new().open(true)) + .expect("reopen should queue"); + } + } + PlatformEvent::Closed { window_id: id } if id == window_id => { + counts.lock().unwrap().2 += 1; + } + _ => {} + } + + let (configured, opened, closed) = *counts.lock().unwrap(); + if configured >= 2 && opened >= 2 && closed >= 1 { + proxy.shutdown().expect("shutdown should queue"); + break; + } + } + + while platform.next_event().await.is_some() {} + *done.lock().unwrap() = true; + let _ = main.queue_task(|| {}); + } + }); + + run(); + let (configured, opened, closed) = *counts.lock().unwrap(); + assert_eq!(configured, 2); + assert_eq!(opened, 2); + assert_eq!(closed, 1); + assert!(*done.lock().unwrap()); + } +} diff --git a/lib/ui/src/runtime.rs b/lib/ui/src/runtime.rs new file mode 100644 index 0000000..200be60 --- /dev/null +++ b/lib/ui/src/runtime.rs @@ -0,0 +1,238 @@ +//! Explicit-construction UI runtime helpers. + +use ruin_reactivity::{EffectHandle, effect}; + +use crate::platform::{PlatformClosed, PlatformEvent, PlatformProxy, PlatformRuntime}; +use crate::scene::SceneSnapshot; +use crate::window::{WindowId, WindowSpec, WindowUpdate}; + +/// High-level UI-side owner of platform event consumption. +/// +/// The headless backend currently co-locates platform and rendering on one worker thread, but this +/// runtime type deliberately treats them as logical subsystems behind an event/snapshot boundary so +/// future backends can split them without rewriting the app-facing control flow. +pub struct UiRuntime { + platform: PlatformRuntime, +} + +/// Explicit handle for one declarative window instance. +/// +/// The controller owns no native resources directly. Instead, it issues commands to the platform +/// runtime and can host a reactive scene effect that pushes immutable scene snapshots whenever UI +/// state changes. +#[derive(Clone)] +pub struct WindowController { + id: WindowId, + proxy: PlatformProxy, +} + +/// Returned when the platform event stream closes before a matching event is observed. +#[derive(Clone, Copy, Debug, Eq, PartialEq)] +pub struct EventStreamClosed; + +impl std::fmt::Display for EventStreamClosed { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + f.write_str("platform event stream closed") + } +} + +impl std::error::Error for EventStreamClosed {} + +impl UiRuntime { + /// Creates a UI runtime backed by the headless prototype backend. + pub fn headless() -> Self { + Self { + platform: PlatformRuntime::headless(), + } + } + + /// Returns a cloneable proxy for low-level platform commands. + pub fn proxy(&self) -> PlatformProxy { + self.platform.proxy() + } + + /// Creates a new declarative window and returns a controller for it. + pub fn create_window(&self, spec: WindowSpec) -> Result { + let id = self.proxy().create_window(spec)?; + Ok(WindowController { + id, + proxy: self.proxy(), + }) + } + + /// Waits for the next platform event. + pub async fn next_event(&mut self) -> Option { + self.platform.next_event().await + } + + /// Waits until an event matches `predicate`. + pub async fn wait_for_event_matching( + &mut self, + mut predicate: impl FnMut(&PlatformEvent) -> bool, + ) -> Result { + while let Some(event) = self.next_event().await { + if predicate(&event) { + return Ok(event); + } + } + Err(EventStreamClosed) + } + + /// Requests shutdown of the platform runtime. + pub fn shutdown(&self) -> Result<(), PlatformClosed> { + self.proxy().shutdown() + } +} + +impl WindowController { + /// Returns the logical window identifier. + pub const fn id(&self) -> WindowId { + self.id + } + + /// Applies a window update. + pub fn update(&self, update: WindowUpdate) -> Result<(), PlatformClosed> { + self.proxy.update_window(self.id, update) + } + + /// Replaces the latest retained scene for this window. + pub fn replace_scene(&self, scene: SceneSnapshot) -> Result<(), PlatformClosed> { + self.proxy.replace_scene(self.id, scene) + } + + /// Emits a close-request event for this window. + pub fn emit_close_requested(&self) -> Result<(), PlatformClosed> { + self.proxy.emit_close_requested(self.id) + } + + /// Attaches a reactive effect that rebuilds and replaces the window scene whenever dependent UI + /// state changes. + pub fn attach_scene_effect( + &self, + build_scene: impl Fn() -> SceneSnapshot + 'static, + ) -> EffectHandle { + let controller = self.clone(); + effect(move || { + let scene = build_scene(); + controller + .replace_scene(scene) + .expect("window controller should remain alive while scene effect is attached"); + }) + } +} + +#[cfg(test)] +mod tests { + use super::UiRuntime; + use crate::platform::PlatformEvent; + use crate::scene::{Color, Point, PreparedText, Rect, SceneSnapshot, UiSize}; + use crate::window::{WindowSpec, WindowUpdate}; + use ruin_runtime::{current_thread_handle, queue_future, run}; + use std::future::Future; + + fn run_async_test(future: impl Future + 'static) { + let main = current_thread_handle(); + queue_future(async move { + future.await; + let _ = main.queue_task(|| {}); + }); + run(); + } + + #[test] + fn visibility_restore_re_presents_latest_retained_scene() { + run_async_test(async move { + let mut ui = UiRuntime::headless(); + let window = ui + .create_window(WindowSpec::new("visibility-policy").visible(true)) + .expect("window should be created"); + + let mut scene = SceneSnapshot::new(1, UiSize::new(320.0, 180.0)); + scene + .push_quad( + Rect::new(0.0, 0.0, 320.0, 180.0), + Color::rgb(0x20, 0x22, 0x35), + ) + .push_text(PreparedText::monospace( + "retained scene", + Point::new(16.0, 28.0), + 16.0, + 8.0, + Color::rgb(0xFF, 0xFF, 0xFF), + )); + window + .replace_scene(scene) + .expect("scene replacement should queue"); + + let _ = ui + .wait_for_event_matching(|event| { + matches!( + event, + PlatformEvent::FramePresented { + window_id, + scene_version: 1, + .. + } if *window_id == window.id() + ) + }) + .await + .expect("initial present should occur"); + + window + .update(WindowUpdate::new().visible(false)) + .expect("hide should queue"); + let _ = ui + .wait_for_event_matching(|event| { + matches!( + event, + PlatformEvent::VisibilityChanged { + window_id, + visible: false, + } if *window_id == window.id() + ) + }) + .await + .expect("hide event should occur"); + + window + .update(WindowUpdate::new().visible(true)) + .expect("show should queue"); + let _ = ui + .wait_for_event_matching(|event| { + matches!( + event, + PlatformEvent::VisibilityChanged { + window_id, + visible: true, + } if *window_id == window.id() + ) + }) + .await + .expect("show event should occur"); + + let event = ui + .wait_for_event_matching(|event| { + matches!( + event, + PlatformEvent::FramePresented { + window_id, + scene_version: 1, + .. + } if *window_id == window.id() + ) + }) + .await + .expect("visibility restore should re-present latest retained scene"); + + assert!(matches!( + event, + PlatformEvent::FramePresented { + scene_version: 1, + .. + } + )); + + ui.shutdown().expect("shutdown should queue"); + }); + } +} diff --git a/lib/ui/src/scene.rs b/lib/ui/src/scene.rs new file mode 100644 index 0000000..1dec444 --- /dev/null +++ b/lib/ui/src/scene.rs @@ -0,0 +1,210 @@ +//! Renderer-oriented scene snapshot types. + +use tracing::debug; + +use crate::trace_targets; + +pub type SceneVersion = u64; + +#[derive(Clone, Copy, Debug, PartialEq)] +pub struct Point { + pub x: f32, + pub y: f32, +} + +impl Point { + pub const fn new(x: f32, y: f32) -> Self { + Self { x, y } + } +} + +#[derive(Clone, Copy, Debug, PartialEq)] +pub struct UiSize { + pub width: f32, + pub height: f32, +} + +impl UiSize { + pub const fn new(width: f32, height: f32) -> Self { + Self { width, height } + } +} + +#[derive(Clone, Copy, Debug, PartialEq)] +pub struct Rect { + pub origin: Point, + pub size: UiSize, +} + +impl Rect { + pub const fn new(x: f32, y: f32, width: f32, height: f32) -> Self { + Self { + origin: Point::new(x, y), + size: UiSize::new(width, height), + } + } +} + +#[derive(Clone, Copy, Debug, Eq, PartialEq)] +pub struct Color { + pub r: u8, + pub g: u8, + pub b: u8, + pub a: u8, +} + +impl Color { + pub const fn rgba(r: u8, g: u8, b: u8, a: u8) -> Self { + Self { r, g, b, a } + } + + pub const fn rgb(r: u8, g: u8, b: u8) -> Self { + Self::rgba(r, g, b, 255) + } +} + +#[derive(Clone, Copy, Debug, PartialEq)] +pub struct Translation { + pub x: f32, + pub y: f32, +} + +impl Translation { + pub const fn new(x: f32, y: f32) -> Self { + Self { x, y } + } +} + +#[derive(Clone, Copy, Debug, PartialEq)] +pub struct Quad { + pub rect: Rect, + pub color: Color, +} + +impl Quad { + pub const fn new(rect: Rect, color: Color) -> Self { + Self { rect, color } + } +} + +#[derive(Clone, Debug, PartialEq)] +pub struct GlyphInstance { + pub glyph: String, + pub position: Point, + pub advance: f32, +} + +#[derive(Clone, Debug, PartialEq)] +pub struct PreparedText { + pub text: String, + pub font_size: f32, + pub color: Color, + pub glyphs: Vec, +} + +impl PreparedText { + pub fn monospace( + text: impl Into, + origin: Point, + font_size: f32, + advance: f32, + color: Color, + ) -> Self { + let text = text.into(); + let mut x = origin.x; + let mut glyphs = Vec::with_capacity(text.chars().count()); + for ch in text.chars() { + glyphs.push(GlyphInstance { + glyph: ch.to_string(), + position: Point::new(x, origin.y), + advance, + }); + x += advance; + } + + Self { + text, + font_size, + color, + glyphs, + } + } +} + +#[derive(Clone, Debug, PartialEq)] +pub enum DisplayItem { + Quad(Quad), + Text(PreparedText), + PushClip(Rect), + PopClip, + PushTransform(Translation), + PopTransform, + LayerBegin { opacity: f32 }, + LayerEnd, +} + +#[derive(Clone, Debug, PartialEq)] +pub struct SceneSnapshot { + pub version: SceneVersion, + pub logical_size: UiSize, + pub items: Vec, +} + +impl SceneSnapshot { + pub fn new(version: SceneVersion, logical_size: UiSize) -> Self { + debug!( + target: trace_targets::SCENE, + event = "create_scene", + version, + width = logical_size.width, + height = logical_size.height, + "creating scene snapshot" + ); + Self { + version, + logical_size, + items: Vec::new(), + } + } + + pub fn item_count(&self) -> usize { + self.items.len() + } + + pub fn push_item(&mut self, item: DisplayItem) -> &mut Self { + self.items.push(item); + self + } + + pub fn push_quad(&mut self, rect: Rect, color: Color) -> &mut Self { + self.push_item(DisplayItem::Quad(Quad::new(rect, color))) + } + + pub fn push_text(&mut self, text: PreparedText) -> &mut Self { + self.push_item(DisplayItem::Text(text)) + } + + pub fn push_clip(&mut self, rect: Rect) -> &mut Self { + self.push_item(DisplayItem::PushClip(rect)) + } + + pub fn pop_clip(&mut self) -> &mut Self { + self.push_item(DisplayItem::PopClip) + } + + pub fn push_transform(&mut self, translation: Translation) -> &mut Self { + self.push_item(DisplayItem::PushTransform(translation)) + } + + pub fn pop_transform(&mut self) -> &mut Self { + self.push_item(DisplayItem::PopTransform) + } + + pub fn begin_layer(&mut self, opacity: f32) -> &mut Self { + self.push_item(DisplayItem::LayerBegin { opacity }) + } + + pub fn end_layer(&mut self) -> &mut Self { + self.push_item(DisplayItem::LayerEnd) + } +} diff --git a/lib/ui/src/window.rs b/lib/ui/src/window.rs new file mode 100644 index 0000000..38950fe --- /dev/null +++ b/lib/ui/src/window.rs @@ -0,0 +1,235 @@ +//! Common window model types. + +use crate::scene::UiSize; + +#[derive(Clone, Copy, Debug, Eq, Hash, Ord, PartialEq, PartialOrd)] +pub struct WindowId(u64); + +impl WindowId { + pub const fn from_raw(raw: u64) -> Self { + Self(raw) + } + + pub const fn raw(self) -> u64 { + self.0 + } +} + +#[derive(Clone, Copy, Debug, Eq, PartialEq)] +pub enum DecorationMode { + Auto, + Server, + Client, +} + +#[derive(Clone, Copy, Debug, Eq, PartialEq)] +pub enum WindowLifecycle { + LogicalClosed, + Opening, + AwaitingInitialConfigure, + OpenHidden, + OpenVisible, + Closing, +} + +#[derive(Clone, Copy, Debug, PartialEq)] +pub struct WindowConfigured { + pub actual_inner_size: UiSize, + pub scale_factor: f32, + pub visible: bool, + pub maximized: bool, + pub fullscreen: bool, +} + +#[derive(Clone, Debug, PartialEq)] +pub struct WindowSpec { + pub key: Option, + pub title: String, + pub app_id: Option, + pub open: bool, + pub visible: bool, + pub requested_inner_size: Option, + pub min_inner_size: Option, + pub max_inner_size: Option, + pub decorations: DecorationMode, + pub maximized: bool, + pub fullscreen: bool, + pub resizable: bool, +} + +impl WindowSpec { + pub fn new(title: impl Into) -> Self { + Self { + key: None, + title: title.into(), + app_id: None, + open: true, + visible: true, + requested_inner_size: None, + min_inner_size: None, + max_inner_size: None, + decorations: DecorationMode::Auto, + maximized: false, + fullscreen: false, + resizable: true, + } + } + + pub fn key(mut self, key: impl Into) -> Self { + self.key = Some(key.into()); + self + } + + pub fn app_id(mut self, app_id: impl Into) -> Self { + self.app_id = Some(app_id.into()); + self + } + + pub fn open(mut self, open: bool) -> Self { + self.open = open; + self + } + + pub fn visible(mut self, visible: bool) -> Self { + self.visible = visible; + self + } + + pub fn requested_inner_size(mut self, size: UiSize) -> Self { + self.requested_inner_size = Some(size); + self + } + + pub fn min_inner_size(mut self, size: UiSize) -> Self { + self.min_inner_size = Some(size); + self + } + + pub fn max_inner_size(mut self, size: UiSize) -> Self { + self.max_inner_size = Some(size); + self + } + + pub fn decorations(mut self, decorations: DecorationMode) -> Self { + self.decorations = decorations; + self + } + + pub fn maximized(mut self, maximized: bool) -> Self { + self.maximized = maximized; + self + } + + pub fn fullscreen(mut self, fullscreen: bool) -> Self { + self.fullscreen = fullscreen; + self + } + + pub fn resizable(mut self, resizable: bool) -> Self { + self.resizable = resizable; + self + } +} + +#[derive(Clone, Debug, Default, PartialEq)] +pub struct WindowUpdate { + pub title: Option, + pub open: Option, + pub visible: Option, + pub requested_inner_size: Option>, + pub min_inner_size: Option>, + pub max_inner_size: Option>, + pub decorations: Option, + pub maximized: Option, + pub fullscreen: Option, + pub resizable: Option, +} + +impl WindowUpdate { + pub fn new() -> Self { + Self::default() + } + + pub fn title(mut self, title: impl Into) -> Self { + self.title = Some(title.into()); + self + } + + pub fn open(mut self, open: bool) -> Self { + self.open = Some(open); + self + } + + pub fn visible(mut self, visible: bool) -> Self { + self.visible = Some(visible); + self + } + + pub fn requested_inner_size(mut self, size: Option) -> Self { + self.requested_inner_size = Some(size); + self + } + + pub fn min_inner_size(mut self, size: Option) -> Self { + self.min_inner_size = Some(size); + self + } + + pub fn max_inner_size(mut self, size: Option) -> Self { + self.max_inner_size = Some(size); + self + } + + pub fn decorations(mut self, decorations: DecorationMode) -> Self { + self.decorations = Some(decorations); + self + } + + pub fn maximized(mut self, maximized: bool) -> Self { + self.maximized = Some(maximized); + self + } + + pub fn fullscreen(mut self, fullscreen: bool) -> Self { + self.fullscreen = Some(fullscreen); + self + } + + pub fn resizable(mut self, resizable: bool) -> Self { + self.resizable = Some(resizable); + self + } + + pub(crate) fn apply_to(&self, spec: &mut WindowSpec) { + if let Some(title) = &self.title { + spec.title = title.clone(); + } + if let Some(open) = self.open { + spec.open = open; + } + if let Some(visible) = self.visible { + spec.visible = visible; + } + if let Some(requested_inner_size) = self.requested_inner_size { + spec.requested_inner_size = requested_inner_size; + } + if let Some(min_inner_size) = self.min_inner_size { + spec.min_inner_size = min_inner_size; + } + if let Some(max_inner_size) = self.max_inner_size { + spec.max_inner_size = max_inner_size; + } + if let Some(decorations) = self.decorations { + spec.decorations = decorations; + } + if let Some(maximized) = self.maximized { + spec.maximized = maximized; + } + if let Some(fullscreen) = self.fullscreen { + spec.fullscreen = fullscreen; + } + if let Some(resizable) = self.resizable { + spec.resizable = resizable; + } + } +} diff --git a/lib/ui_platform_wayland/Cargo.toml b/lib/ui_platform_wayland/Cargo.toml new file mode 100644 index 0000000..be7a9be --- /dev/null +++ b/lib/ui_platform_wayland/Cargo.toml @@ -0,0 +1,11 @@ +[package] +name = "ruin_ui_platform_wayland" +version = "0.1.0" +edition = "2024" + +[dependencies] +raw-window-handle = "0.6" +ruin_ui = { path = "../ui" } +wayland-backend = { version = "0.3", features = ["client_system"] } +wayland-client = "0.31" +wayland-protocols = { version = "0.32", features = ["client"] } diff --git a/lib/ui_platform_wayland/src/lib.rs b/lib/ui_platform_wayland/src/lib.rs new file mode 100644 index 0000000..8b0a6eb --- /dev/null +++ b/lib/ui_platform_wayland/src/lib.rs @@ -0,0 +1,271 @@ +use std::error::Error; +use std::ffi::c_void; +use std::num::NonZeroU32; +use std::ptr::NonNull; + +use raw_window_handle::{ + DisplayHandle, HandleError, HasDisplayHandle, HasWindowHandle, RawDisplayHandle, + RawWindowHandle, WaylandDisplayHandle, WaylandWindowHandle, WindowHandle, +}; +use ruin_ui::{UiSize, WindowSpec}; +use wayland_client::globals::{GlobalListContents, registry_queue_init}; +use wayland_client::protocol::{wl_compositor, wl_registry, wl_surface}; +use wayland_client::{Connection, Dispatch, Proxy, QueueHandle, delegate_noop}; +use wayland_protocols::xdg::shell::client::{xdg_surface, xdg_toplevel, xdg_wm_base}; + +#[derive(Clone)] +pub struct WaylandSurfaceTarget { + connection: Connection, + surface: wl_surface::WlSurface, +} + +impl HasDisplayHandle for WaylandSurfaceTarget { + fn display_handle(&self) -> Result, HandleError> { + let ptr = NonNull::new(self.connection.backend().display_ptr().cast::()) + .ok_or(HandleError::Unavailable)?; + let raw = RawDisplayHandle::Wayland(WaylandDisplayHandle::new(ptr)); + Ok(unsafe { DisplayHandle::borrow_raw(raw) }) + } +} + +impl HasWindowHandle for WaylandSurfaceTarget { + fn window_handle(&self) -> Result, HandleError> { + let ptr = NonNull::new(self.surface.id().as_ptr().cast::()) + .ok_or(HandleError::Unavailable)?; + let raw = RawWindowHandle::Wayland(WaylandWindowHandle::new(ptr)); + Ok(unsafe { WindowHandle::borrow_raw(raw) }) + } +} + +#[derive(Clone, Copy, Debug, Eq, PartialEq)] +pub struct FrameRequest { + pub width: u32, + pub height: u32, + pub resized: bool, +} + +pub struct WaylandWindow { + event_queue: wayland_client::EventQueue, + surface_target: WaylandSurfaceTarget, + state: State, +} + +struct State { + running: bool, + _connection: Connection, + _compositor: wl_compositor::WlCompositor, + _surface: wl_surface::WlSurface, + _xdg_surface: xdg_surface::XdgSurface, + _toplevel: xdg_toplevel::XdgToplevel, + _wm_base: xdg_wm_base::XdgWmBase, + current_size: (u32, u32), + configured: bool, + pending_size: Option<(u32, u32)>, + needs_redraw: bool, +} + +impl State { + fn request_redraw(&mut self) { + self.needs_redraw = true; + } +} + +impl WaylandWindow { + pub fn open(spec: WindowSpec) -> Result> { + let connection = Connection::connect_to_env()?; + let (globals, event_queue) = registry_queue_init::(&connection)?; + let qh = event_queue.handle(); + + let compositor: wl_compositor::WlCompositor = globals.bind(&qh, 4..=6, ())?; + let wm_base: xdg_wm_base::XdgWmBase = globals.bind(&qh, 1..=6, ())?; + let surface = compositor.create_surface(&qh, ()); + let xdg_surface = wm_base.get_xdg_surface(&surface, &qh, ()); + let toplevel = xdg_surface.get_toplevel(&qh, ()); + toplevel.set_title(spec.title.clone()); + if let Some(app_id) = spec.app_id.as_ref() { + toplevel.set_app_id(app_id.clone()); + } + + apply_size_constraints(&toplevel, &spec); + if spec.maximized { + toplevel.set_maximized(); + } + if spec.fullscreen { + toplevel.set_fullscreen(None); + } + + surface.commit(); + connection.flush()?; + + let initial_size = spec + .requested_inner_size + .unwrap_or_else(|| UiSize::new(800.0, 500.0)); + let initial_width = initial_size.width.max(1.0).round() as u32; + let initial_height = initial_size.height.max(1.0).round() as u32; + let surface_target = WaylandSurfaceTarget { + connection: connection.clone(), + surface: surface.clone(), + }; + + Ok(Self { + event_queue, + surface_target, + state: State { + running: true, + _connection: connection, + _compositor: compositor, + _surface: surface, + _xdg_surface: xdg_surface, + _toplevel: toplevel, + _wm_base: wm_base, + current_size: (initial_width, initial_height), + configured: false, + pending_size: None, + needs_redraw: false, + }, + }) + } + + pub fn surface_target(&self) -> WaylandSurfaceTarget { + self.surface_target.clone() + } + + pub fn is_running(&self) -> bool { + self.state.running + } + + pub fn dispatch(&mut self) -> Result<(), Box> { + self.event_queue.blocking_dispatch(&mut self.state)?; + Ok(()) + } + + pub fn request_redraw(&mut self) { + self.state.request_redraw(); + } + + pub fn prepare_frame(&mut self) -> Option { + if !self.state.configured { + return None; + } + + if let Some((width, height)) = self.state.pending_size.take() { + self.state.current_size = (width, height); + self.state.needs_redraw = false; + return Some(FrameRequest { + width, + height, + resized: true, + }); + } + + if self.state.needs_redraw { + self.state.needs_redraw = false; + return Some(FrameRequest { + width: self.state.current_size.0, + height: self.state.current_size.1, + resized: false, + }); + } + + None + } +} + +impl Dispatch for State { + fn event( + _state: &mut Self, + _proxy: &wl_registry::WlRegistry, + _event: wl_registry::Event, + _data: &GlobalListContents, + _conn: &Connection, + _qh: &QueueHandle, + ) { + } +} + +delegate_noop!(State: ignore wl_compositor::WlCompositor); +delegate_noop!(State: ignore wl_surface::WlSurface); + +impl Dispatch for State { + fn event( + _state: &mut Self, + wm_base: &xdg_wm_base::XdgWmBase, + event: xdg_wm_base::Event, + _data: &(), + _conn: &Connection, + _qh: &QueueHandle, + ) { + if let xdg_wm_base::Event::Ping { serial } = event { + wm_base.pong(serial); + } + } +} + +impl Dispatch for State { + fn event( + state: &mut Self, + xdg_surface: &xdg_surface::XdgSurface, + event: xdg_surface::Event, + _data: &(), + _conn: &Connection, + _qh: &QueueHandle, + ) { + if let xdg_surface::Event::Configure { serial } = event { + xdg_surface.ack_configure(serial); + state.configured = true; + state.request_redraw(); + } + } +} + +impl Dispatch for State { + fn event( + state: &mut Self, + _proxy: &xdg_toplevel::XdgToplevel, + event: xdg_toplevel::Event, + _data: &(), + _conn: &Connection, + _qh: &QueueHandle, + ) { + match event { + xdg_toplevel::Event::Close => { + state.running = false; + } + xdg_toplevel::Event::Configure { + width, + height, + states: _, + } => { + let width = NonZeroU32::new(width as u32) + .map(NonZeroU32::get) + .unwrap_or(state.current_size.0); + let height = NonZeroU32::new(height as u32) + .map(NonZeroU32::get) + .unwrap_or(state.current_size.1); + state.pending_size = Some((width, height)); + state.request_redraw(); + } + _ => {} + } + } +} + +fn apply_size_constraints(toplevel: &xdg_toplevel::XdgToplevel, spec: &WindowSpec) { + if spec.resizable { + let min = spec.min_inner_size.unwrap_or_else(|| UiSize::new(0.0, 0.0)); + let max = spec.max_inner_size.unwrap_or_else(|| UiSize::new(0.0, 0.0)); + toplevel.set_min_size(min.width.round() as i32, min.height.round() as i32); + toplevel.set_max_size(max.width.round() as i32, max.height.round() as i32); + return; + } + + let fixed = spec + .requested_inner_size + .or(spec.min_inner_size) + .or(spec.max_inner_size) + .unwrap_or_else(|| UiSize::new(800.0, 500.0)); + let width = fixed.width.max(1.0).round() as i32; + let height = fixed.height.max(1.0).round() as i32; + toplevel.set_min_size(width, height); + toplevel.set_max_size(width, height); +} diff --git a/lib/ui_renderer_wgpu/Cargo.toml b/lib/ui_renderer_wgpu/Cargo.toml new file mode 100644 index 0000000..59b32f8 --- /dev/null +++ b/lib/ui_renderer_wgpu/Cargo.toml @@ -0,0 +1,11 @@ +[package] +name = "ruin_ui_renderer_wgpu" +version = "0.1.0" +edition = "2024" + +[dependencies] +bytemuck = { version = "1", features = ["derive"] } +pollster = "0.4" +raw-window-handle = "0.6" +ruin_ui = { path = "../ui" } +wgpu = "29" diff --git a/lib/ui_renderer_wgpu/src/lib.rs b/lib/ui_renderer_wgpu/src/lib.rs new file mode 100644 index 0000000..735b0ee --- /dev/null +++ b/lib/ui_renderer_wgpu/src/lib.rs @@ -0,0 +1,317 @@ +use std::error::Error; + +use bytemuck::{Pod, Zeroable}; +use raw_window_handle::{HasDisplayHandle, HasWindowHandle}; +use ruin_ui::{Color, DisplayItem, Rect, SceneSnapshot}; +use wgpu::util::DeviceExt; + +#[repr(C)] +#[derive(Clone, Copy, Pod, Zeroable)] +struct Vertex { + position: [f32; 2], + color: [f32; 4], +} + +const VERTEX_ATTRIBUTES: [wgpu::VertexAttribute; 2] = + wgpu::vertex_attr_array![0 => Float32x2, 1 => Float32x4]; + +impl Vertex { + const LAYOUT: wgpu::VertexBufferLayout<'static> = wgpu::VertexBufferLayout { + array_stride: std::mem::size_of::() as u64, + step_mode: wgpu::VertexStepMode::Vertex, + attributes: &VERTEX_ATTRIBUTES, + }; + + fn layout() -> wgpu::VertexBufferLayout<'static> { + Self::LAYOUT + } +} + +#[derive(Clone, Copy, Debug, Eq, PartialEq)] +pub enum RenderError { + Lost, + Outdated, + Timeout, + Occluded, + Validation, +} + +pub struct WgpuSceneRenderer { + surface: wgpu::Surface<'static>, + device: wgpu::Device, + queue: wgpu::Queue, + config: wgpu::SurfaceConfiguration, + pipeline: wgpu::RenderPipeline, +} + +impl WgpuSceneRenderer { + pub fn new( + target: impl HasDisplayHandle + HasWindowHandle + Send + Sync + 'static, + width: u32, + height: u32, + ) -> Result> { + let instance = wgpu::Instance::new(wgpu::InstanceDescriptor::new_without_display_handle()); + let surface = instance.create_surface(target)?; + let adapter = pollster::block_on(instance.request_adapter(&wgpu::RequestAdapterOptions { + power_preference: wgpu::PowerPreference::HighPerformance, + force_fallback_adapter: false, + compatible_surface: Some(&surface), + }))?; + let (device, queue) = + pollster::block_on(adapter.request_device(&wgpu::DeviceDescriptor::default()))?; + + let caps = surface.get_capabilities(&adapter); + let format = caps + .formats + .iter() + .copied() + .find(wgpu::TextureFormat::is_srgb) + .unwrap_or(caps.formats[0]); + let config = wgpu::SurfaceConfiguration { + usage: wgpu::TextureUsages::RENDER_ATTACHMENT, + format, + width: width.max(1), + height: height.max(1), + present_mode: wgpu::PresentMode::AutoVsync, + desired_maximum_frame_latency: 2, + alpha_mode: caps.alpha_modes[0], + view_formats: vec![], + }; + surface.configure(&device, &config); + + let shader = device.create_shader_module(wgpu::ShaderModuleDescriptor { + label: Some("ruin-ui-renderer-wgpu-shader"), + source: wgpu::ShaderSource::Wgsl( + r#" +struct VertexIn { + @location(0) position: vec2, + @location(1) color: vec4, +}; + +struct VertexOut { + @builtin(position) position: vec4, + @location(0) color: vec4, +}; + +@vertex +fn vs_main(input: VertexIn) -> VertexOut { + var out: VertexOut; + out.position = vec4(input.position, 0.0, 1.0); + out.color = input.color; + return out; +} + +@fragment +fn fs_main(input: VertexOut) -> @location(0) vec4 { + return input.color; +} +"# + .into(), + ), + }); + + let pipeline_layout = device.create_pipeline_layout(&wgpu::PipelineLayoutDescriptor { + label: Some("ruin-ui-renderer-wgpu-pipeline-layout"), + bind_group_layouts: &[], + immediate_size: 0, + }); + let pipeline = device.create_render_pipeline(&wgpu::RenderPipelineDescriptor { + label: Some("ruin-ui-renderer-wgpu-pipeline"), + layout: Some(&pipeline_layout), + vertex: wgpu::VertexState { + module: &shader, + entry_point: Some("vs_main"), + buffers: &[Vertex::layout()], + compilation_options: wgpu::PipelineCompilationOptions::default(), + }, + fragment: Some(wgpu::FragmentState { + module: &shader, + entry_point: Some("fs_main"), + targets: &[Some(wgpu::ColorTargetState { + format, + blend: Some(wgpu::BlendState::ALPHA_BLENDING), + write_mask: wgpu::ColorWrites::ALL, + })], + compilation_options: wgpu::PipelineCompilationOptions::default(), + }), + primitive: wgpu::PrimitiveState::default(), + depth_stencil: None, + multisample: wgpu::MultisampleState::default(), + multiview_mask: None, + cache: None, + }); + + Ok(Self { + surface, + device, + queue, + config, + pipeline, + }) + } + + pub fn size(&self) -> (u32, u32) { + (self.config.width, self.config.height) + } + + pub fn resize(&mut self, width: u32, height: u32) { + self.config.width = width.max(1); + self.config.height = height.max(1); + self.surface.configure(&self.device, &self.config); + } + + pub fn render(&mut self, scene: &SceneSnapshot) -> Result<(), RenderError> { + let vertices = build_vertices(scene); + let frame = match self.surface.get_current_texture() { + wgpu::CurrentSurfaceTexture::Success(frame) + | wgpu::CurrentSurfaceTexture::Suboptimal(frame) => frame, + wgpu::CurrentSurfaceTexture::Lost => return Err(RenderError::Lost), + wgpu::CurrentSurfaceTexture::Outdated => return Err(RenderError::Outdated), + wgpu::CurrentSurfaceTexture::Timeout => return Err(RenderError::Timeout), + wgpu::CurrentSurfaceTexture::Occluded => return Err(RenderError::Occluded), + wgpu::CurrentSurfaceTexture::Validation => return Err(RenderError::Validation), + }; + let view = frame + .texture + .create_view(&wgpu::TextureViewDescriptor::default()); + let vertex_buffer = self + .device + .create_buffer_init(&wgpu::util::BufferInitDescriptor { + label: Some("ruin-ui-renderer-wgpu-vertices"), + contents: bytemuck::cast_slice(&vertices), + usage: wgpu::BufferUsages::VERTEX, + }); + let mut encoder = self + .device + .create_command_encoder(&wgpu::CommandEncoderDescriptor { + label: Some("ruin-ui-renderer-wgpu-encoder"), + }); + { + let mut pass = encoder.begin_render_pass(&wgpu::RenderPassDescriptor { + label: Some("ruin-ui-renderer-wgpu-pass"), + color_attachments: &[Some(wgpu::RenderPassColorAttachment { + view: &view, + depth_slice: None, + resolve_target: None, + ops: wgpu::Operations { + load: wgpu::LoadOp::Clear(wgpu::Color { + r: 0.03, + g: 0.04, + b: 0.08, + a: 1.0, + }), + store: wgpu::StoreOp::Store, + }, + })], + depth_stencil_attachment: None, + timestamp_writes: None, + occlusion_query_set: None, + multiview_mask: None, + }); + if !vertices.is_empty() { + pass.set_pipeline(&self.pipeline); + pass.set_vertex_buffer(0, vertex_buffer.slice(..)); + pass.draw(0..vertices.len() as u32, 0..1); + } + } + self.queue.submit([encoder.finish()]); + frame.present(); + Ok(()) + } +} + +fn build_vertices(scene: &SceneSnapshot) -> Vec { + let width = scene.logical_size.width.max(1.0); + let height = scene.logical_size.height.max(1.0); + let mut vertices = Vec::new(); + + for item in &scene.items { + if let DisplayItem::Quad(quad) = item { + push_quad_vertices(&mut vertices, quad.rect, quad.color, width, height); + } + } + + vertices +} + +fn push_quad_vertices( + vertices: &mut Vec, + rect: Rect, + color: Color, + width: f32, + height: f32, +) { + let left = to_ndc_x(rect.origin.x, width); + let right = to_ndc_x(rect.origin.x + rect.size.width, width); + let top = to_ndc_y(rect.origin.y, height); + let bottom = to_ndc_y(rect.origin.y + rect.size.height, height); + let color = [ + color.r as f32 / 255.0, + color.g as f32 / 255.0, + color.b as f32 / 255.0, + color.a as f32 / 255.0, + ]; + + vertices.extend_from_slice(&[ + Vertex { + position: [left, top], + color, + }, + Vertex { + position: [right, top], + color, + }, + Vertex { + position: [right, bottom], + color, + }, + Vertex { + position: [left, top], + color, + }, + Vertex { + position: [right, bottom], + color, + }, + Vertex { + position: [left, bottom], + color, + }, + ]); +} + +fn to_ndc_x(x: f32, width: f32) -> f32 { + (x / width) * 2.0 - 1.0 +} + +fn to_ndc_y(y: f32, height: f32) -> f32 { + 1.0 - (y / height) * 2.0 +} + +#[cfg(test)] +mod tests { + use super::build_vertices; + use ruin_ui::{Color, PreparedText, Rect, SceneSnapshot, UiSize}; + + #[test] + fn quad_scenes_expand_to_six_vertices_per_quad() { + let mut scene = SceneSnapshot::new(1, UiSize::new(100.0, 50.0)); + scene.push_quad( + Rect::new(0.0, 0.0, 100.0, 50.0), + Color::rgb(0x11, 0x22, 0x33), + ); + scene.push_quad( + Rect::new(10.0, 10.0, 20.0, 20.0), + Color::rgb(0x44, 0x55, 0x66), + ); + scene.push_text(PreparedText { + text: "ignored".into(), + font_size: 16.0, + color: Color::rgb(0xFF, 0xFF, 0xFF), + glyphs: Vec::new(), + }); + + let vertices = build_vertices(&scene); + assert_eq!(vertices.len(), 12); + } +}