diff --git a/Cargo.lock b/Cargo.lock index 6809d3e..536bea8 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -2,6 +2,22 @@ # It is not intended for manual editing. version = 4 +[[package]] +name = "ab_glyph" +version = "0.2.32" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "01c0457472c38ea5bd1c3b5ada5e368271cb550be7a4ca4a0b4634e9913f6cc2" +dependencies = [ + "ab_glyph_rasterizer", + "owned_ttf_parser", +] + +[[package]] +name = "ab_glyph_rasterizer" +version = "0.1.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "366ffbaa4442f4684d91e2cd7c5ea7c4ed8add41959a31447066e279e432b618" + [[package]] name = "addr2line" version = "0.25.1" @@ -63,6 +79,31 @@ version = "0.2.21" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "683d7910e743518b0e34f1186f92494becacb047c7b6bf616c96772180fef923" +[[package]] +name = "android-activity" +version = "0.6.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0f2a1bb052857d5dd49572219344a7332b31b76405648eabac5bc68978251bcd" +dependencies = [ + "android-properties", + "bitflags 2.10.0", + "cc", + "jni", + "libc", + "log", + "ndk", + "ndk-context", + "ndk-sys", + "num_enum", + "thiserror 2.0.18", +] + +[[package]] +name = "android-properties" +version = "0.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fc7eb209b1518d6bb87b283c20095f5228ecda460da70b44f0802523dea6da04" + [[package]] name = "android_system_properties" version = "0.1.5" @@ -74,7 +115,7 @@ dependencies = [ [[package]] name = "andromeda" -version = "0.1.5" +version = "0.1.6" dependencies = [ "andromeda-core", "andromeda-runtime", @@ -126,7 +167,7 @@ dependencies = [ [[package]] name = "andromeda-core" -version = "0.1.5" +version = "0.1.6" dependencies = [ "anyhow", "anymap", @@ -150,7 +191,7 @@ dependencies = [ [[package]] name = "andromeda-runtime" -version = "0.1.5" +version = "0.1.6" dependencies = [ "andromeda-core", "anyhow", @@ -169,7 +210,8 @@ dependencies = [ "nova_vm", "oxc-miette", "oxc_diagnostics", - "rand 0.10.0", + "rand 0.10.1", + "raw-window-handle", "ring", "rusqlite", "rustls", @@ -178,7 +220,7 @@ dependencies = [ "saffron", "serde", "serde_json", - "signal-hook 0.4.3", + "signal-hook 0.4.4", "socket2 0.6.3", "swash", "tempfile", @@ -190,21 +232,7 @@ dependencies = [ "uuid", "webpki-roots", "wgpu", -] - -[[package]] -name = "anstream" -version = "0.6.21" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "43d5b281e737544384e969a5ccad3f1cdd24b48086a0fc1b2a5262a26b8f4f4a" -dependencies = [ - "anstyle", - "anstyle-parse 0.2.7", - "anstyle-query", - "anstyle-wincon", - "colorchoice", - "is_terminal_polyfill", - "utf8parse", + "winit", ] [[package]] @@ -214,7 +242,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "824a212faf96e9acacdbd09febd34438f8f711fb84e09a8916013cd7815ca28d" dependencies = [ "anstyle", - "anstyle-parse 1.0.0", + "anstyle-parse", "anstyle-query", "anstyle-wincon", "colorchoice", @@ -228,15 +256,6 @@ version = "1.0.13" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "5192cca8006f1fd4f7237516f40fa183bb07f8fbdfedaa0036de5ea9b0b45e78" -[[package]] -name = "anstyle-parse" -version = "0.2.7" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4e7644824f0aa2c7b9384579234ef10eb7efb6a0deb83f9630a49594dd9c15c2" -dependencies = [ - "utf8parse", -] - [[package]] name = "anstyle-parse" version = "1.0.0" @@ -316,12 +335,24 @@ version = "0.5.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "7d902e3d592a523def97af8f317b08ce16b7ab854c1985a0c671e6f15cebc236" +[[package]] +name = "arrayref" +version = "0.3.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "76a2e8124351fda1ef8aaaa3bbd7ebbcb486bbcd4225aca0aa0d84bb2db8fecb" + [[package]] name = "arrayvec" version = "0.7.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "7c02d123df017efcdfbd739ef81735b36c5ba83ec3c59c80a9d7ecc718f92e50" +[[package]] +name = "as-raw-xcb-connection" +version = "1.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "175571dd1d178ced59193a6fc02dde1b972eb0bc56c892cde9beeceac5bf0f6b" + [[package]] name = "as-slice" version = "0.2.1" @@ -565,6 +596,15 @@ dependencies = [ "generic-array", ] +[[package]] +name = "block2" +version = "0.5.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2c132eebf10f5cad5289222520a4a058514204aed6d791f1cf4fe8088b82d15f" +dependencies = [ + "objc2", +] + [[package]] name = "built" version = "0.8.0" @@ -638,6 +678,32 @@ dependencies = [ "displaydoc", ] +[[package]] +name = "calloop" +version = "0.13.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b99da2f8558ca23c71f4fd15dc57c906239752dd27ff3c00a1d56b685b7cbfec" +dependencies = [ + "bitflags 2.10.0", + "log", + "polling", + "rustix 0.38.44", + "slab", + "thiserror 1.0.69", +] + +[[package]] +name = "calloop-wayland-source" +version = "0.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "95a66a987056935f7efce4ab5668920b5d0dac4a7c99991a67395f13702ddd20" +dependencies = [ + "calloop", + "rustix 0.38.44", + "wayland-backend", + "wayland-client", +] + [[package]] name = "capacity_builder" version = "0.5.0" @@ -724,9 +790,9 @@ checksum = "6e4de3bc4ea267985becf712dc6d9eed8b04c953b3fcfb339ebc87acd9804901" [[package]] name = "clap" -version = "4.6.0" +version = "4.6.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b193af5b67834b676abd72466a96c1024e6a6ad978a1f484bd90b85c94041351" +checksum = "1ddb117e43bbf7dacf0a4190fef4d345b9bad68dfc649cb349e7d17d28428e51" dependencies = [ "clap_builder", "clap_derive", @@ -738,7 +804,7 @@ version = "4.6.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "714a53001bf66416adb0e2ef5ac857140e7dc3a0c48fb28b2f10762fc4b5069f" dependencies = [ - "anstream 1.0.0", + "anstream", "anstyle", "clap_lex", "strsim", @@ -746,18 +812,18 @@ dependencies = [ [[package]] name = "clap_complete" -version = "4.6.0" +version = "4.6.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "19c9f1dde76b736e3681f28cec9d5a61299cbaae0fce80a68e43724ad56031eb" +checksum = "3ff7a1dccbdd8b078c2bdebff47e404615151534d5043da397ec50286816f9cb" dependencies = [ "clap", ] [[package]] name = "clap_derive" -version = "4.6.0" +version = "4.6.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1110bd8a634a1ab8cb04345d8d878267d57c3cf1b38d91b71af6686408bbca6a" +checksum = "f2ce8604710f6733aa641a2b3731eaa1e8b3d9973d5e3565da11800813f997a9" dependencies = [ "heck", "proc-macro2", @@ -845,6 +911,15 @@ dependencies = [ "static_assertions", ] +[[package]] +name = "concurrent-queue" +version = "2.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4ca0197aee26d1ae37445ee532fefce43251d24cc7c166799f4d46817f1d3973" +dependencies = [ + "crossbeam-utils", +] + [[package]] name = "console" version = "0.16.3" @@ -932,6 +1007,30 @@ version = "0.8.7" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "773648b94d0e5d620f64f280777445740e61fe701025087ec8b57f45c791888b" +[[package]] +name = "core-graphics" +version = "0.23.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c07782be35f9e1140080c6b96f0d44b739e2278479f64e02fdab4e32dfd8b081" +dependencies = [ + "bitflags 1.3.2", + "core-foundation 0.9.4", + "core-graphics-types 0.1.3", + "foreign-types 0.5.0", + "libc", +] + +[[package]] +name = "core-graphics-types" +version = "0.1.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "45390e6114f68f718cc7a830514a96f903cccd70d02a8f6d9f643ac4ba45afaf" +dependencies = [ + "bitflags 1.3.2", + "core-foundation 0.9.4", + "libc", +] + [[package]] name = "core-graphics-types" version = "0.2.0" @@ -976,7 +1075,7 @@ dependencies = [ "rustc-hash 2.1.1", "self_cell", "skrifa 0.40.0", - "smol_str", + "smol_str 0.3.6", "swash", "sys-locale", "unicode-bidi", @@ -1064,7 +1163,7 @@ dependencies = [ "document-features", "mio", "parking_lot", - "rustix", + "rustix 1.1.4", "serde", "signal-hook 0.3.18", "signal-hook-mio", @@ -1096,6 +1195,12 @@ dependencies = [ "typenum", ] +[[package]] +name = "cursor-icon" +version = "1.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f27ae1dd37df86211c42e150270f82743308803d90a6f6e6651cd730d5e1732f" + [[package]] name = "dashmap" version = "5.5.3" @@ -1256,6 +1361,12 @@ dependencies = [ "winapi", ] +[[package]] +name = "dispatch" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bd0c93bb4b0c6d9b77f4435b0ae98c24d17f1c45b2ff844c6151a07256ca923b" + [[package]] name = "displaydoc" version = "0.2.5" @@ -1267,6 +1378,15 @@ dependencies = [ "syn 2.0.117", ] +[[package]] +name = "dlib" +version = "0.5.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ab8ecd87370524b461f8557c119c405552c396ed91fc0a8eec68679eab26f94a" +dependencies = [ + "libloading 0.8.9", +] + [[package]] name = "document-features" version = "0.2.12" @@ -1290,6 +1410,18 @@ dependencies = [ "zerocopy 0.8.31", ] +[[package]] +name = "downcast-rs" +version = "1.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "75b325c5dbd37f80359721ad39aca5a29fb04c89279657cffdda8736d0c0b9d2" + +[[package]] +name = "dpi" +version = "0.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d8b14ccef22fc6f5a8f4d7d768562a182c04ce9a3b3157b91390b52ddfdf1a76" + [[package]] name = "dprint-core" version = "0.67.4" @@ -1471,11 +1603,11 @@ dependencies = [ [[package]] name = "env_logger" -version = "0.11.9" +version = "0.11.10" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b2daee4ea451f429a58296525ddf28b45a3b64f1acf6587e2067437bb11e218d" +checksum = "0621c04f2196ac3f488dd583365b9c09be011a4ab8b9f37248ffcc8f6198b56a" dependencies = [ - "anstream 0.6.21", + "anstream", "anstyle", "env_filter", "jiff", @@ -1606,7 +1738,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "0ce92ff622d6dadf7349484f42c93271a0d49b7cc4d466a936405bacbe10aa78" dependencies = [ "cfg-if", - "rustix", + "rustix 1.1.4", "windows-sys 0.59.0", ] @@ -1868,6 +2000,16 @@ dependencies = [ "version_check", ] +[[package]] +name = "gethostname" +version = "1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1bd49230192a3797a9a4d6abe9b3eed6f7fa4c8a8a4947977c6f80025f92cbd8" +dependencies = [ + "rustix 1.1.4", + "windows-link", +] + [[package]] name = "getrandom" version = "0.2.16" @@ -2097,6 +2239,17 @@ dependencies = [ "foldhash 0.2.0", ] +[[package]] +name = "hashbrown" +version = "0.17.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4f467dd6dccf739c208452f8014c75c18bb8301b050ad1cfb27153803edb0f51" +dependencies = [ + "allocator-api2", + "equivalent", + "foldhash 0.2.0", +] + [[package]] name = "hashlink" version = "0.10.0" @@ -2145,9 +2298,9 @@ checksum = "dfa686283ad6dd069f105e5ab091b04c62850d3e4cf5d67debad1933f55023df" [[package]] name = "hotpath" -version = "0.14.0" +version = "0.15.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2fde50be006a0fe95cc2fd6d25d884aa6932218e4055d7df2fa0d95c386acf8d" +checksum = "28594d8a3d30371e1488dcd6ba545bf07e9ce9dff6d8a57d70dbbaa221d3246d" dependencies = [ "arc-swap", "cfg-if", @@ -2158,6 +2311,7 @@ dependencies = [ "futures-util", "hdrhistogram", "hotpath-macros", + "hotpath-meta", "libc", "mach2", "pin-project-lite", @@ -2172,15 +2326,30 @@ dependencies = [ [[package]] name = "hotpath-macros" -version = "0.14.0" +version = "0.15.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "dd884cee056e269e41e1127549458e1c4e309f31897ebbc1416982a74d40a5b5" +checksum = "c7b87d58f92c09858091f328521db329955e9fc960c3c9c55e5dc0ef228a37b0" dependencies = [ "proc-macro2", "quote", "syn 2.0.117", ] +[[package]] +name = "hotpath-macros-meta" +version = "0.15.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b4fcd1a53780c79401c81008b262470386d6dfee64599b337cae37a9674c082d" + +[[package]] +name = "hotpath-meta" +version = "0.15.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4aadd604c1aac308bda11d4792ae46c2076acd0523e401ee8640f101c0b99653" +dependencies = [ + "hotpath-macros-meta", +] + [[package]] name = "hstr" version = "3.0.3" @@ -2556,12 +2725,12 @@ checksum = "964de6e86d545b246d84badc0fef527924ace5134f30641c203ef52ba83f58d5" [[package]] name = "indexmap" -version = "2.13.0" +version = "2.14.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7714e70437a7dc3ac8eb7e6f8df75fd8eb422675fc7678aff7364301092b1017" +checksum = "d466e9454f08e4a911e14806c24e16fba1b4c121d1ea474396f396069cf949d9" dependencies = [ "equivalent", - "hashbrown 0.16.1", + "hashbrown 0.17.0", "serde", "serde_core", ] @@ -2672,9 +2841,9 @@ checksum = "84de9d95a6d2547d9b77ee3f25fa0ee32e3c3a6484d47a55adebc0439c077992" [[package]] name = "jiff" -version = "0.2.16" +version = "0.2.23" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "49cce2b81f2098e7e3efc35bc2e0a6b7abec9d34128283d7a26fa8f32a6dbb35" +checksum = "1a3546dc96b6d42c5f24902af9e2538e82e39ad350b0c766eb3fbf2d8f3d8359" dependencies = [ "jiff-static", "log", @@ -2685,9 +2854,9 @@ dependencies = [ [[package]] name = "jiff-static" -version = "0.2.16" +version = "0.2.23" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "980af8b43c3ad5d8d349ace167ec8170839f753a42d233ba19e08afe1850fa69" +checksum = "2a8c8b344124222efd714b73bb41f8b5120b27a7cc1c75593a6ff768d9d05aa4" dependencies = [ "proc-macro2", "quote", @@ -2700,12 +2869,61 @@ version = "0.1.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "c900ef84826f1338a557697dc8fc601df9ca9af4ac137c7fb61d4c6f2dfd3076" +[[package]] +name = "jni" +version = "0.22.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5efd9a482cf3a427f00d6b35f14332adc7902ce91efb778580e180ff90fa3498" +dependencies = [ + "cfg-if", + "combine", + "jni-macros", + "jni-sys 0.4.1", + "log", + "simd_cesu8", + "thiserror 2.0.18", + "walkdir", + "windows-link", +] + +[[package]] +name = "jni-macros" +version = "0.22.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a00109accc170f0bdb141fed3e393c565b6f5e072365c3bd58f5b062591560a3" +dependencies = [ + "proc-macro2", + "quote", + "rustc_version", + "simd_cesu8", + "syn 2.0.117", +] + [[package]] name = "jni-sys" version = "0.3.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "8eaf4bc02d17cbdd7ff4c7438cafcdf7fb9a4613313ad11b4f8fefe7d3fa0130" +[[package]] +name = "jni-sys" +version = "0.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c6377a88cb3910bee9b0fa88d4f42e1d2da8e79915598f65fb0c7ee14c878af2" +dependencies = [ + "jni-sys-macros", +] + +[[package]] +name = "jni-sys-macros" +version = "0.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "38c0b942f458fe50cdac086d2f946512305e5631e720728f2a61aabcd47a6264" +dependencies = [ + "quote", + "syn 2.0.117", +] + [[package]] name = "jobserver" version = "0.1.34" @@ -2911,6 +3129,7 @@ checksum = "416f7e718bdb06000964960ffa43b4335ad4012ae8b99060261aa4a8088d5ccb" dependencies = [ "bitflags 2.10.0", "libc", + "redox_syscall 0.5.18", ] [[package]] @@ -2926,9 +3145,9 @@ dependencies = [ [[package]] name = "libsui" -version = "0.13.0" +version = "0.14.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4dd268ef2098e102e82c67d9de9076a2f1cef12ae284a7a35635ea30fa2dda9e" +checksum = "9759fd10709b0b8b0dc066b1ab47a30a52b221b34a3d5ef3af3f12dd5fffc962" dependencies = [ "editpe", "image", @@ -2961,6 +3180,12 @@ version = "0.5.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "0717cef1bc8b636c6e1c1bbdefc09e6322da8a9321966e8928ef80d20f7f770f" +[[package]] +name = "linux-raw-sys" +version = "0.4.15" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d26c52dbd32dccf2d10cac7725f8eae5296885fb5703b261f7d0a0739ec807ab" + [[package]] name = "linux-raw-sys" version = "0.12.1" @@ -3005,11 +3230,11 @@ dependencies = [ [[package]] name = "lru" -version = "0.16.3" +version = "0.17.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a1dc47f592c06f33f8e3aea9591776ec7c9f9e4124778ff8a3c3b87159f7e593" +checksum = "0e0b564323a0fb6d54b864f625ae139de9612e27edb944dda37c109f05aac531" dependencies = [ - "hashbrown 0.16.1", + "hashbrown 0.17.0", ] [[package]] @@ -3095,7 +3320,7 @@ checksum = "00c15a6f673ff72ddcc22394663290f870fb224c1bfce55734a75c414150e605" dependencies = [ "bitflags 2.10.0", "block", - "core-graphics-types", + "core-graphics-types 0.2.0", "foreign-types 0.5.0", "log", "objc", @@ -3120,9 +3345,9 @@ dependencies = [ [[package]] name = "mio" -version = "1.1.1" +version = "1.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a69bcab0ad47271a0234d9422b131806bf3968021e5dc9328caf2d4cd58557fc" +checksum = "50b7e5b27aa02a74bac8c3f23f448f8d87ff11f92d3aac1a6ed369ee08cc56c1" dependencies = [ "libc", "log", @@ -3203,13 +3428,34 @@ dependencies = [ "tempfile", ] +[[package]] +name = "ndk" +version = "0.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c3f42e7bbe13d351b6bead8286a43aac9534b82bd3cc43e47037f012ebfd62d4" +dependencies = [ + "bitflags 2.10.0", + "jni-sys 0.3.0", + "log", + "ndk-sys", + "num_enum", + "raw-window-handle", + "thiserror 1.0.69", +] + +[[package]] +name = "ndk-context" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "27b02d87554356db9e9a873add8782d4ea6e3e58ea071a9adb9a2e8ddb884a8b" + [[package]] name = "ndk-sys" version = "0.6.0+11769913" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ee6cda3051665f1fb8d9e08fc35c96d5a244fb1be711a03b71118828afc9a873" dependencies = [ - "jni-sys", + "jni-sys 0.3.0", ] [[package]] @@ -3270,7 +3516,7 @@ dependencies = [ "oxc_semantic", "oxc_span", "oxc_syntax", - "rand 0.10.0", + "rand 0.10.1", "regex", "ryu-js", "small_string", @@ -3345,19 +3591,244 @@ dependencies = [ name = "num-traits" version = "0.2.19" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "071dfc062690e90b734c0b2273ce72ad0ffa95f0c74596bc250dcfd960262841" +checksum = "071dfc062690e90b734c0b2273ce72ad0ffa95f0c74596bc250dcfd960262841" +dependencies = [ + "autocfg", + "libm", +] + +[[package]] +name = "num_enum" +version = "0.7.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5d0bca838442ec211fa11de3a8b0e0e8f3a4522575b5c4c06ed722e005036f26" +dependencies = [ + "num_enum_derive", + "rustversion", +] + +[[package]] +name = "num_enum_derive" +version = "0.7.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "680998035259dcfcafe653688bf2aa6d3e2dc05e98be6ab46afb089dc84f1df8" +dependencies = [ + "proc-macro-crate", + "proc-macro2", + "quote", + "syn 2.0.117", +] + +[[package]] +name = "objc" +version = "0.2.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "915b1b472bc21c53464d6c8461c9d3af805ba1ef837e1cac254428f4a77177b1" +dependencies = [ + "malloc_buf", +] + +[[package]] +name = "objc-sys" +version = "0.3.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cdb91bdd390c7ce1a8607f35f3ca7151b65afc0ff5ff3b34fa350f7d7c7e4310" + +[[package]] +name = "objc2" +version = "0.5.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "46a785d4eeff09c14c487497c162e92766fbb3e4059a71840cecc03d9a50b804" +dependencies = [ + "objc-sys", + "objc2-encode", +] + +[[package]] +name = "objc2-app-kit" +version = "0.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e4e89ad9e3d7d297152b17d39ed92cd50ca8063a89a9fa569046d41568891eff" +dependencies = [ + "bitflags 2.10.0", + "block2", + "libc", + "objc2", + "objc2-core-data", + "objc2-core-image", + "objc2-foundation", + "objc2-quartz-core", +] + +[[package]] +name = "objc2-cloud-kit" +version = "0.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "74dd3b56391c7a0596a295029734d3c1c5e7e510a4cb30245f8221ccea96b009" +dependencies = [ + "bitflags 2.10.0", + "block2", + "objc2", + "objc2-core-location", + "objc2-foundation", +] + +[[package]] +name = "objc2-contacts" +version = "0.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a5ff520e9c33812fd374d8deecef01d4a840e7b41862d849513de77e44aa4889" +dependencies = [ + "block2", + "objc2", + "objc2-foundation", +] + +[[package]] +name = "objc2-core-data" +version = "0.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "617fbf49e071c178c0b24c080767db52958f716d9eabdf0890523aeae54773ef" +dependencies = [ + "bitflags 2.10.0", + "block2", + "objc2", + "objc2-foundation", +] + +[[package]] +name = "objc2-core-image" +version = "0.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "55260963a527c99f1819c4f8e3b47fe04f9650694ef348ffd2227e8196d34c80" +dependencies = [ + "block2", + "objc2", + "objc2-foundation", + "objc2-metal", +] + +[[package]] +name = "objc2-core-location" +version = "0.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "000cfee34e683244f284252ee206a27953279d370e309649dc3ee317b37e5781" +dependencies = [ + "block2", + "objc2", + "objc2-contacts", + "objc2-foundation", +] + +[[package]] +name = "objc2-encode" +version = "4.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ef25abbcd74fb2609453eb695bd2f860d389e457f67dc17cafc8b8cbc89d0c33" + +[[package]] +name = "objc2-foundation" +version = "0.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0ee638a5da3799329310ad4cfa62fbf045d5f56e3ef5ba4149e7452dcf89d5a8" +dependencies = [ + "bitflags 2.10.0", + "block2", + "dispatch", + "libc", + "objc2", +] + +[[package]] +name = "objc2-link-presentation" +version = "0.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a1a1ae721c5e35be65f01a03b6d2ac13a54cb4fa70d8a5da293d7b0020261398" +dependencies = [ + "block2", + "objc2", + "objc2-app-kit", + "objc2-foundation", +] + +[[package]] +name = "objc2-metal" +version = "0.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dd0cba1276f6023976a406a14ffa85e1fdd19df6b0f737b063b95f6c8c7aadd6" +dependencies = [ + "bitflags 2.10.0", + "block2", + "objc2", + "objc2-foundation", +] + +[[package]] +name = "objc2-quartz-core" +version = "0.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e42bee7bff906b14b167da2bac5efe6b6a07e6f7c0a21a7308d40c960242dc7a" +dependencies = [ + "bitflags 2.10.0", + "block2", + "objc2", + "objc2-foundation", + "objc2-metal", +] + +[[package]] +name = "objc2-symbols" +version = "0.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0a684efe3dec1b305badae1a28f6555f6ddd3bb2c2267896782858d5a78404dc" +dependencies = [ + "objc2", + "objc2-foundation", +] + +[[package]] +name = "objc2-ui-kit" +version = "0.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b8bb46798b20cd6b91cbd113524c490f1686f4c4e8f49502431415f3512e2b6f" +dependencies = [ + "bitflags 2.10.0", + "block2", + "objc2", + "objc2-cloud-kit", + "objc2-core-data", + "objc2-core-image", + "objc2-core-location", + "objc2-foundation", + "objc2-link-presentation", + "objc2-quartz-core", + "objc2-symbols", + "objc2-uniform-type-identifiers", + "objc2-user-notifications", +] + +[[package]] +name = "objc2-uniform-type-identifiers" +version = "0.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "44fa5f9748dbfe1ca6c0b79ad20725a11eca7c2218bceb4b005cb1be26273bfe" dependencies = [ - "autocfg", - "libm", + "block2", + "objc2", + "objc2-foundation", ] [[package]] -name = "objc" -version = "0.2.7" +name = "objc2-user-notifications" +version = "0.2.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "915b1b472bc21c53464d6c8461c9d3af805ba1ef837e1cac254428f4a77177b1" +checksum = "76cfcbf642358e8689af64cee815d139339f3ed8ad05103ed5eaf73db8d84cb3" dependencies = [ - "malloc_buf", + "bitflags 2.10.0", + "block2", + "objc2", + "objc2-core-location", + "objc2-foundation", ] [[package]] @@ -3446,6 +3917,16 @@ dependencies = [ "vcpkg", ] +[[package]] +name = "orbclient" +version = "0.3.53" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "12c6933ddbbd16539a7672e697bb8d41ac3a4e99ac43eeb40c07236bd7fcb2dd" +dependencies = [ + "libc", + "libredox", +] + [[package]] name = "ordered-float" version = "5.1.0" @@ -3461,6 +3942,15 @@ version = "0.5.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "1a80800c0488c3a21695ea981a54918fbb37abf04f4d0720c453632255e2ff0e" +[[package]] +name = "owned_ttf_parser" +version = "0.25.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "36820e9051aca1014ddc75770aab4d68bc1e9e632f0f5627c4086bc216fb583b" +dependencies = [ + "ttf-parser", +] + [[package]] name = "owo-colors" version = "4.3.0" @@ -3877,7 +4367,7 @@ checksum = "2621685985a2ebf1c516881c026032ac7deafcda1a2c9b7850dc81e3dfcb64c1" dependencies = [ "cfg-if", "libc", - "redox_syscall", + "redox_syscall 0.5.18", "smallvec", "windows-link", ] @@ -4085,6 +4575,20 @@ dependencies = [ "miniz_oxide", ] +[[package]] +name = "polling" +version = "3.11.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5d0e4f59085d47d8241c88ead0f274e8a0cb551f3625263c05eb8dd897c34218" +dependencies = [ + "cfg-if", + "concurrent-queue", + "hermit-abi", + "pin-project-lite", + "rustix 1.1.4", + "windows-sys 0.61.2", +] + [[package]] name = "portable-atomic" version = "1.11.1" @@ -4173,6 +4677,15 @@ dependencies = [ "unicode-width 0.1.14", ] +[[package]] +name = "proc-macro-crate" +version = "3.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e67ba7e9b2b56446f1d419b1d807906278ffa1a658a8a5d8a39dcb1f5a78614f" +dependencies = [ + "toml_edit", +] + [[package]] name = "proc-macro2" version = "1.0.106" @@ -4270,6 +4783,15 @@ version = "2.0.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "a993555f31e5a609f617c12db6250dedcac1b0a85076912c436e6fc9b2c8e6a3" +[[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" @@ -4323,9 +4845,9 @@ dependencies = [ [[package]] name = "rand" -version = "0.10.0" +version = "0.10.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "bc266eb313df6c5c09c1c7b1fbe2510961e5bcd3add930c1e31f7ed9da0feff8" +checksum = "d2e8e8bcc7961af1fdac401278c6a831614941f6164ee3bf4ce61b7edb162207" dependencies = [ "chacha20", "getrandom 0.4.2", @@ -4494,6 +5016,15 @@ dependencies = [ "font-types 0.11.1", ] +[[package]] +name = "redox_syscall" +version = "0.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4722d768eff46b75989dd134e5c353f0d6296e5aaa3132e776cbdb56be7731aa" +dependencies = [ + "bitflags 1.3.2", +] + [[package]] name = "redox_syscall" version = "0.5.18" @@ -4749,6 +5280,19 @@ dependencies = [ "semver", ] +[[package]] +name = "rustix" +version = "0.38.44" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fdb5bc1ae2baa591800df16c9ca78619bf65c0488b41b96ccec5d11220d8c154" +dependencies = [ + "bitflags 2.10.0", + "errno", + "libc", + "linux-raw-sys 0.4.15", + "windows-sys 0.59.0", +] + [[package]] name = "rustix" version = "1.1.4" @@ -4758,15 +5302,15 @@ dependencies = [ "bitflags 2.10.0", "errno", "libc", - "linux-raw-sys", + "linux-raw-sys 0.12.1", "windows-sys 0.61.2", ] [[package]] name = "rustls" -version = "0.23.37" +version = "0.23.38" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "758025cb5fccfd3bc2fd74708fd4682be41d99e5dff73c377c0646c6012c73a4" +checksum = "69f9466fb2c14ea04357e91413efb882e2a6d4a406e625449bc0a5d360d53a21" dependencies = [ "aws-lc-rs", "log", @@ -4886,6 +5430,19 @@ dependencies = [ "syn 2.0.117", ] +[[package]] +name = "sctk-adwaita" +version = "0.10.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b6277f0217056f77f1d8f49f2950ac6c278c0d607c45f5ee99328d792ede24ec" +dependencies = [ + "ab_glyph", + "log", + "memmap2", + "smithay-client-toolkit", + "tiny-skia", +] + [[package]] name = "security-framework" version = "2.11.1" @@ -4983,9 +5540,9 @@ dependencies = [ [[package]] name = "serde_spanned" -version = "1.1.0" +version = "1.1.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "876ac351060d4f882bb1032b6369eb0aef79ad9df1ea8bc404874d8cc3d0cd98" +checksum = "6662b5879511e06e8999a8a235d848113e942c9124f211511b16466ee2995f26" dependencies = [ "serde_core", ] @@ -5082,9 +5639,9 @@ dependencies = [ [[package]] name = "signal-hook" -version = "0.4.3" +version = "0.4.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3b57709da74f9ff9f4a27dce9526eec25ca8407c45a7887243b031a58935fb8e" +checksum = "b2a0c28ca5908dbdbcd52e6fdaa00358ab88637f8ab33e1f188dd510eb44b53d" dependencies = [ "libc", "signal-hook-registry", @@ -5116,6 +5673,16 @@ version = "0.3.8" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e320a6c5ad31d271ad523dcf3ad13e2767ad8b1cb8f047f75a8aeaf8da139da2" +[[package]] +name = "simd_cesu8" +version = "1.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "94f90157bb87cddf702797c5dadfa0be7d266cdf49e22da2fcaa32eff75b2c33" +dependencies = [ + "rustc_version", + "simdutf8", +] + [[package]] name = "simd_helpers" version = "0.1.0" @@ -5209,6 +5776,40 @@ version = "0.3.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b7c388c1b5e93756d0c740965c41e8822f866621d41acbdf6336a6a168f8840c" +[[package]] +name = "smithay-client-toolkit" +version = "0.19.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3457dea1f0eb631b4034d61d4d8c32074caa6cd1ab2d59f2327bd8461e2c0016" +dependencies = [ + "bitflags 2.10.0", + "calloop", + "calloop-wayland-source", + "cursor-icon", + "libc", + "log", + "memmap2", + "rustix 0.38.44", + "thiserror 1.0.69", + "wayland-backend", + "wayland-client", + "wayland-csd-frame", + "wayland-cursor", + "wayland-protocols", + "wayland-protocols-wlr", + "wayland-scanner", + "xkeysym", +] + +[[package]] +name = "smol_str" +version = "0.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dd538fb6910ac1099850255cf94a94df6551fbdd602454387d0adb2d1ca6dead" +dependencies = [ + "serde", +] + [[package]] name = "smol_str" version = "0.3.6" @@ -5347,6 +5948,12 @@ version = "0.4.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d08889ec5408683408db66ad89e0e1f93dff55c73a4ccc71c427d5b277ee47e6" +[[package]] +name = "strict-num" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6637bab7722d379c8b41ba849228d680cc12d0a45ba1fa2b48f2a30577a06731" + [[package]] name = "string_enum" version = "1.0.2" @@ -5643,7 +6250,7 @@ dependencies = [ "fastrand", "getrandom 0.4.2", "once_cell", - "rustix", + "rustix 1.1.4", "windows-sys 0.61.2", ] @@ -5691,7 +6298,7 @@ version = "0.4.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "60b8cb979cb11c32ce1603f8137b22262a9d131aaa5c37b5678025f22b8becd0" dependencies = [ - "rustix", + "rustix 1.1.4", "windows-sys 0.60.2", ] @@ -5839,6 +6446,31 @@ dependencies = [ "zerovec", ] +[[package]] +name = "tiny-skia" +version = "0.11.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "83d13394d44dae3207b52a326c0c85a8bf87f1541f23b0d143811088497b09ab" +dependencies = [ + "arrayref", + "arrayvec", + "bytemuck", + "cfg-if", + "log", + "tiny-skia-path", +] + +[[package]] +name = "tiny-skia-path" +version = "0.11.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9c9e7fc0c2e86a30b117d0462aa261b72b7a99b7ebd7deb3a14ceda95c5bdc93" +dependencies = [ + "arrayref", + "bytemuck", + "strict-num", +] + [[package]] name = "tiny_http" version = "0.12.0" @@ -5879,9 +6511,9 @@ checksum = "1f3ccbac311fea05f86f61904b462b55fb3df8837a366dfc601a0161d0532f20" [[package]] name = "tokio" -version = "1.50.0" +version = "1.52.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "27ad5e34374e03cfffefc301becb44e9dc3c17584f414349ebe29ed26661822d" +checksum = "b67dee974fe86fd92cc45b7a95fdd2f99a36a6d7b0d431a231178d3d670bbcc6" dependencies = [ "bytes", "libc", @@ -5895,9 +6527,9 @@ dependencies = [ [[package]] name = "tokio-macros" -version = "2.6.0" +version = "2.7.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "af407857209536a95c8e56f8231ef2c2e2aff839b22e07a1ffcbc617e9db9fa5" +checksum = "385a6cb71ab9ab790c5fe8d67f1645e6c450a7ce006a33de03daa956cf70a496" dependencies = [ "proc-macro2", "quote", @@ -5961,9 +6593,9 @@ dependencies = [ [[package]] name = "toml" -version = "1.1.0+spec-1.1.0" +version = "1.1.2+spec-1.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f8195ca05e4eb728f4ba94f3e3291661320af739c4e43779cbdfae82ab239fcc" +checksum = "81f3d15e84cbcd896376e6730314d59fb5a87f31e4b038454184435cd57defee" dependencies = [ "indexmap", "serde_core", @@ -5976,27 +6608,39 @@ dependencies = [ [[package]] name = "toml_datetime" -version = "1.1.0+spec-1.1.0" +version = "1.1.1+spec-1.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "97251a7c317e03ad83774a8752a7e81fb6067740609f75ea2b585b569a59198f" +checksum = "3165f65f62e28e0115a00b2ebdd37eb6f3b641855f9d636d3cd4103767159ad7" dependencies = [ "serde_core", ] +[[package]] +name = "toml_edit" +version = "0.25.8+spec-1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "16bff38f1d86c47f9ff0647e6838d7bb362522bdf44006c7068c2b1e606f1f3c" +dependencies = [ + "indexmap", + "toml_datetime", + "toml_parser", + "winnow", +] + [[package]] name = "toml_parser" -version = "1.1.0+spec-1.1.0" +version = "1.1.2+spec-1.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2334f11ee363607eb04df9b8fc8a13ca1715a72ba8662a26ac285c98aabb4011" +checksum = "a2abe9b86193656635d2411dc43050282ca48aa31c2451210f4202550afb7526" dependencies = [ "winnow", ] [[package]] name = "toml_writer" -version = "1.1.0+spec-1.1.0" +version = "1.1.1+spec-1.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d282ade6016312faf3e41e57ebbba0c073e4056dab1232ab1cb624199648f8ed" +checksum = "756daf9b1013ebe47a8776667b466417e2d4c5679d441c26230efd9ef78692db" [[package]] name = "tower" @@ -6422,9 +7066,9 @@ checksum = "06abde3611657adf66d383f00b093d7faecc7fa57071cce2578660c9f1010821" [[package]] name = "uuid" -version = "1.22.0" +version = "1.23.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a68d3c8f01c0cfa54a75291d83601161799e4a89a39e0929f4b0354d88757a37" +checksum = "ddd74a9687298c6858e9b88ec8935ec45d22e8fd5e6394fa1bd4e99a87789c76" dependencies = [ "getrandom 0.4.2", "js-sys", @@ -6613,6 +7257,115 @@ dependencies = [ "semver", ] +[[package]] +name = "wayland-backend" +version = "0.3.15" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2857dd20b54e916ec7253b3d6b4d5c4d7d4ca2c33c2e11c6c76a99bd8744755d" +dependencies = [ + "cc", + "downcast-rs", + "rustix 1.1.4", + "scoped-tls", + "smallvec", + "wayland-sys", +] + +[[package]] +name = "wayland-client" +version = "0.31.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "645c7c96bb74690c3189b5c9cb4ca1627062bb23693a4fad9d8c3de958260144" +dependencies = [ + "bitflags 2.10.0", + "rustix 1.1.4", + "wayland-backend", + "wayland-scanner", +] + +[[package]] +name = "wayland-csd-frame" +version = "0.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "625c5029dbd43d25e6aa9615e88b829a5cad13b2819c4ae129fdbb7c31ab4c7e" +dependencies = [ + "bitflags 2.10.0", + "cursor-icon", + "wayland-backend", +] + +[[package]] +name = "wayland-cursor" +version = "0.31.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4a52d18780be9b1314328a3de5f930b73d2200112e3849ca6cb11822793fb34d" +dependencies = [ + "rustix 1.1.4", + "wayland-client", + "xcursor", +] + +[[package]] +name = "wayland-protocols" +version = "0.32.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "563a85523cade2429938e790815fd7319062103b9f4a2dc806e9b53b95982d8f" +dependencies = [ + "bitflags 2.10.0", + "wayland-backend", + "wayland-client", + "wayland-scanner", +] + +[[package]] +name = "wayland-protocols-plasma" +version = "0.3.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2b6d8cf1eb2c1c31ed1f5643c88a6e53538129d4af80030c8cabd1f9fa884d91" +dependencies = [ + "bitflags 2.10.0", + "wayland-backend", + "wayland-client", + "wayland-protocols", + "wayland-scanner", +] + +[[package]] +name = "wayland-protocols-wlr" +version = "0.3.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "eb04e52f7836d7c7976c78ca0250d61e33873c34156a2a1fc9474828ec268234" +dependencies = [ + "bitflags 2.10.0", + "wayland-backend", + "wayland-client", + "wayland-protocols", + "wayland-scanner", +] + +[[package]] +name = "wayland-scanner" +version = "0.31.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9c324a910fd86ebdc364a3e61ec1f11737d3b1d6c273c0239ee8ff4bc0d24b4a" +dependencies = [ + "proc-macro2", + "quick-xml", + "quote", +] + +[[package]] +name = "wayland-sys" +version = "0.31.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d8eab23fefc9e41f8e841df4a9c707e8a8c4ed26e944ef69297184de2785e3be" +dependencies = [ + "dlib", + "log", + "once_cell", + "pkg-config", +] + [[package]] name = "web-sys" version = "0.3.83" @@ -6635,9 +7388,9 @@ dependencies = [ [[package]] name = "webpki-roots" -version = "1.0.6" +version = "1.0.7" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "22cfaf3c063993ff62e73cb4311efde4db1efb31ab78a3e5c457939ad5cc0bed" +checksum = "52f5ee44c96cf55f1b349600768e3ece3a8f26010c05265ab73f945bb1a2eb9d" dependencies = [ "rustls-pki-types", ] @@ -6751,7 +7504,7 @@ dependencies = [ "bytemuck", "cfg-if", "cfg_aliases", - "core-graphics-types", + "core-graphics-types 0.2.0", "glow", "glutin_wgl_sys", "gpu-alloc", @@ -7201,11 +7954,66 @@ version = "0.53.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d6bbff5f0aada427a1e5a6da5f1f98158182f26556f345ac9e04d36d0ebed650" +[[package]] +name = "winit" +version = "0.30.13" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a6755fa58a9f8350bd1e472d4c3fcc25f824ec358933bba33306d0b63df5978d" +dependencies = [ + "ahash", + "android-activity", + "atomic-waker", + "bitflags 2.10.0", + "block2", + "bytemuck", + "calloop", + "cfg_aliases", + "concurrent-queue", + "core-foundation 0.9.4", + "core-graphics", + "cursor-icon", + "dpi", + "js-sys", + "libc", + "memmap2", + "ndk", + "objc2", + "objc2-app-kit", + "objc2-foundation", + "objc2-ui-kit", + "orbclient", + "percent-encoding", + "pin-project", + "raw-window-handle", + "redox_syscall 0.4.1", + "rustix 0.38.44", + "sctk-adwaita", + "smithay-client-toolkit", + "smol_str 0.2.2", + "tracing", + "unicode-segmentation", + "wasm-bindgen", + "wasm-bindgen-futures", + "wayland-backend", + "wayland-client", + "wayland-protocols", + "wayland-protocols-plasma", + "web-sys", + "web-time", + "windows-sys 0.52.0", + "x11-dl", + "x11rb", + "xkbcommon-dl", +] + [[package]] name = "winnow" version = "1.0.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "a90e88e4667264a994d34e6d1ab2d26d398dcdca8b7f52bec8668957517fc7d8" +dependencies = [ + "memchr", +] [[package]] name = "winreg" @@ -7323,6 +8131,63 @@ version = "0.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "c01ae8492c38f52376efd3a17d0994b6bcf3df1e39c0226d458b7d81670b2a06" +[[package]] +name = "x11-dl" +version = "2.21.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "38735924fedd5314a6e548792904ed8c6de6636285cb9fec04d5b1db85c1516f" +dependencies = [ + "libc", + "once_cell", + "pkg-config", +] + +[[package]] +name = "x11rb" +version = "0.13.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9993aa5be5a26815fe2c3eacfc1fde061fc1a1f094bf1ad2a18bf9c495dd7414" +dependencies = [ + "as-raw-xcb-connection", + "gethostname", + "libc", + "libloading 0.8.9", + "once_cell", + "rustix 1.1.4", + "x11rb-protocol", +] + +[[package]] +name = "x11rb-protocol" +version = "0.13.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ea6fc2961e4ef194dcbfe56bb845534d0dc8098940c7e5c012a258bfec6701bd" + +[[package]] +name = "xcursor" +version = "0.3.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bec9e4a500ca8864c5b47b8b482a73d62e4237670e5b5f1d6b9e3cae50f28f2b" + +[[package]] +name = "xkbcommon-dl" +version = "0.4.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d039de8032a9a8856a6be89cea3e5d12fdd82306ab7c94d74e6deab2460651c5" +dependencies = [ + "bitflags 2.10.0", + "dlib", + "log", + "once_cell", + "xkeysym", +] + +[[package]] +name = "xkeysym" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b9cc00251562a284751c9973bace760d86c0276c471b4be569fe6b068ee97a56" + [[package]] name = "xml-rs" version = "0.8.28" diff --git a/Cargo.toml b/Cargo.toml index b8d5a10..b1e848a 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -7,7 +7,7 @@ authors = ["the Andromeda team"] edition = "2024" license = "Mozilla Public License 2.0" repository = "https://github.com/tryandromeda/andromeda" -version = "0.1.5" +version = "0.1.6" [workspace.dependencies] andromeda-core = { path = "crates/core" } @@ -25,24 +25,24 @@ chrono = { version = "0.4.44", features = ["serde"] } clap = { version = "4.6.0", features = ["derive"] } clap_complete = "4.6.0" console = "0.16.3" -toml = "1.1.0" +toml = "1.1.2" copilot-client = "0.1.0" cosmic-text = "0.18.2" dprint-core = "0.67.4" dprint-plugin-typescript = "0.95.15" dprint-plugin-json = "0.21.3" -env_logger = "0.11.9" +env_logger = "0.11.10" futures = "0.3.32" glob = "0.3.3" -hotpath = { version = "0.14" } -indexmap = "2.13.0" +hotpath = { version = "0.15" } +indexmap = "2.14.0" image = "0.25.10" lazy_static = "1.5.0" libloading = "0.9.0" libffi = "5.1.0" -libsui = "0.13.0" +libsui = "0.14.0" log = "0.4.29" -lru = "0.16.3" +lru = "0.17.0" lsp-types = "0.97.0" nova_vm = { git = "https://github.com/trynova/nova", rev = "a82b0408533bc93f857aa2ee5daee4f39f62dc6f" } nu-ansi-term = "0.50.3" @@ -58,10 +58,10 @@ oxc_parser = "0.122.0" oxc_semantic = "0.122.0" oxc_span = "0.122.0" oxc_transformer = "0.122.0" -rand = "0.10.0" +rand = "0.10.1" reedline = "0.46.0" regex = "1.12.3" -rustls = "0.23.37" +rustls = "0.23.38" rustls-pemfile = "2.2.0" rustls-pki-types = "1.14.0" ring = "0.17.14" @@ -78,18 +78,20 @@ socket2 = "0.6.3" swash = "0.2.6" trust-dns-resolver = "0.23.2" -signal-hook = "0.4.3" +signal-hook = "0.4.4" thiserror = "2.0.18" tempfile = "3.27.0" -tokio = { version = "1.50.0", features = ["rt", "sync", "time", "fs"] } +tokio = { version = "1.52.1", features = ["rt", "sync", "time", "fs"] } tokio-rustls = "0.26.4" tokio-test = "0.4.5" tower-lsp = "0.20.0" ureq = { version = "3.3.0", features = ["json"] } url = { version = "2.5.8", features = ["serde", "expose_internals"] } -uuid = { version = "1.22.0", features = ["v4"] } -webpki-roots = "1.0.6" +uuid = { version = "1.23.1", features = ["v4"] } +webpki-roots = "1.0.7" wgpu = { version = "27.0.1", features = ["wgsl", "webgpu"] } +winit = "0.30" +raw-window-handle = "0.6" [profile.dev] split-debuginfo = "packed" diff --git a/README.md b/README.md index cb5d8a2..753cea9 100644 --- a/README.md +++ b/README.md @@ -386,6 +386,38 @@ or disabled as needed: | **Time** | Timing utilities | `performance.now()`, `setTimeout()`, `setInterval()`, `Andromeda.sleep()` | | **URL** | URL parsing and manipulation | `URL`, `URLSearchParams` | | **Web** | Web standards | `TextEncoder`, `TextDecoder`, `navigator`, `queueMicrotask()` | +| **Window** *(optional)* | Native OS windowing (winit) | `Andromeda.Window`, `Andromeda.createWindow()`, DOM-style events, `rawHandle()` | + +### Window extension (optional, behind `window` feature) + +The `window` feature adds `Andromeda.Window` — a native OS window backed by +[`winit`](https://crates.io/crates/winit) on macOS, Windows, and Linux +(X11/Wayland). Inspired by [`deno-windowing/dwm`](https://github.com/deno-windowing/dwm), +the class extends `EventTarget` and dispatches DOM-style events (`resize`, +`close`, `keydown`, `keyup`, `mousemove`, `mousedown`, `mouseup`). + +```ts +const win = Andromeda.createWindow({ title: "Hello", width: 640, height: 480 }); +win.addEventListener("keydown", (e) => { + if ((e as CustomEvent).detail.code === "Escape") win.close(); +}); +await Andromeda.Window.mainloop(); +``` + +Enable the feature: + +```bash +cargo run --features window -- run examples/window.ts +``` + +`window.rawHandle()` returns a `{ system, windowHandle, displayHandle, width, height }` +object compatible with `Deno.UnsafeWindowSurface` for future WebGPU-surface bridges. + +When the `canvas` feature is also enabled, `window.presentCanvas(canvas)` blits an +`OffscreenCanvas`'s latest frame into the window via a shared wgpu device — no CPU +readback, any size allowed. `examples/window.ts` demonstrates a live 2D scene streamed +from an `OffscreenCanvas` into a winit-backed window. `examples/breakout.ts` is a +keyboard-driven Breakout clone exercising the full window + canvas input/render pipeline. ## Andromeda Satellites diff --git a/crates/cli/Cargo.toml b/crates/cli/Cargo.toml index 0b1ce87..32895ff 100644 --- a/crates/cli/Cargo.toml +++ b/crates/cli/Cargo.toml @@ -9,7 +9,7 @@ repository.workspace = true readme = "../../README.md" [features] -default = [] +default = ["window"] hotpath = [ "hotpath/hotpath", "andromeda-core/hotpath", @@ -17,6 +17,7 @@ hotpath = [ ] hotpath-alloc = ["hotpath/hotpath-alloc"] llm = ["andromeda-core/llm"] +window = ["andromeda-runtime/window"] [lib] name = "andromeda" diff --git a/crates/cli/src/error.rs b/crates/cli/src/error.rs index 8f9200c..272a0aa 100644 --- a/crates/cli/src/error.rs +++ b/crates/cli/src/error.rs @@ -575,6 +575,16 @@ impl From for CliError { source_code: None, error_span: None, }, + RuntimeError::WindowError { + operation, message, .. + } => CliError::RuntimeError { + message: format!("Window error during {}: {}", operation, message), + file_path: None, + line: None, + column: None, + source_code: None, + error_span: None, + }, RuntimeError::InternalError { message, .. } => CliError::RuntimeError { message: format!("Internal error: {}", message), file_path: None, diff --git a/crates/cli/src/main.rs b/crates/cli/src/main.rs index 4ab0f7c..65d8595 100644 --- a/crates/cli/src/main.rs +++ b/crates/cli/src/main.rs @@ -290,7 +290,7 @@ fn run_main() -> CliResult<()> { Cli::parse() }; - let rt = tokio::runtime::Builder::new_current_thread() + let rt = tokio::runtime::Builder::new_multi_thread() .enable_time() .enable_io() .build() @@ -301,9 +301,9 @@ fn run_main() -> CliResult<()> { Some(Box::new(e)), ) })?; + let _tokio_guard = rt.enter(); - // Run Nova in a secondary blocking thread so tokio tasks can still run - let nova_thread = rt.spawn_blocking(move || -> CliResult<()> { + let nova_result: CliResult<()> = (move || -> CliResult<()> { match args.command { Command::Run { verbose, @@ -491,15 +491,9 @@ fn run_main() -> CliResult<()> { Command::Task { task_name } => run_task(task_name).map_err(|e| *e), Command::Config { action } => handle_config_command(action), } - }); - match rt.block_on(nova_thread) { - Ok(result) => result, - Err(e) => Err(error::CliError::config_error( - "Runtime execution failed".to_string(), - None, - Some(Box::new(e)), - )), - } + })(); + drop(_tokio_guard); + nova_result } fn generate_completions(shell: Option) { diff --git a/crates/cli/src/run.rs b/crates/cli/src/run.rs index da63d97..7d98aec 100644 --- a/crates/cli/src/run.rs +++ b/crates/cli/src/run.rs @@ -110,6 +110,7 @@ pub fn run_with_config( eventloop_handler: recommended_eventloop_handler, macro_task_rx, import_map, + pre_tick_hook: andromeda_runtime::recommended_pre_tick_hook(), }, host_data, ); diff --git a/crates/core/src/error.rs b/crates/core/src/error.rs index d766fcd..4f61527 100644 --- a/crates/core/src/error.rs +++ b/crates/core/src/error.rs @@ -492,6 +492,23 @@ pub enum RuntimeError { source_code: Option>, }, + /// Windowing / native window subsystem errors (winit-backed extension) + #[diagnostic( + code(andromeda::window::error), + help( + "šŸ” Check that the window feature is enabled and the window hasn't been closed.\nšŸ’” Verify the platform supports native windowing.\n🪟 Ensure window operations run on the main thread." + ), + url("https://docs.andromeda.dev/window") + )] + WindowError { + operation: String, + message: String, + #[label("🪟 Window operation failed here")] + error_location: Option, + #[source_code] + source_code: Option>, + }, + /// Internal error (should not happen in normal operation) #[diagnostic( code(andromeda::internal::error), @@ -1220,7 +1237,15 @@ impl RuntimeError { } } - // -------------------- Internal Errors -------------------- + /// Create a new window error + pub fn window_error(operation: impl Into, message: impl Into) -> Self { + Self::WindowError { + operation: operation.into(), + message: message.into(), + error_location: None, + source_code: None, + } + } /// Create an internal error pub fn internal_error(message: impl Into) -> Self { @@ -1341,6 +1366,11 @@ impl fmt::Display for RuntimeError { RuntimeError::LlmNetworkError { message, .. } => { write!(f, "LLM network error: {message}") } + RuntimeError::WindowError { + operation, message, .. + } => { + write!(f, "Window error during {operation}: {message}") + } RuntimeError::InternalError { message, .. } => { write!(f, "Internal error: {message}") } @@ -1798,6 +1828,11 @@ macro_rules! runtime_error { $crate::RuntimeError::llm_network_error($msg) }; + // Window errors + (window: $op:expr, $msg:expr) => { + $crate::RuntimeError::window_error($op, $msg) + }; + // Internal errors (internal: $msg:expr) => { $crate::RuntimeError::internal_error($msg) diff --git a/crates/core/src/runtime.rs b/crates/core/src/runtime.rs index 25fac3d..f0b6141 100644 --- a/crates/core/src/runtime.rs +++ b/crates/core/src/runtime.rs @@ -649,6 +649,11 @@ impl RuntimeFile { } } +/// Called once per iteration of the main `Runtime::run` loop, after promise +/// jobs and timeouts have been drained and before the runtime blocks waiting +/// on macro tasks. +pub type PreTickHook = Box) + 'static>; + pub struct RuntimeConfig { /// Disable or not strict mode. pub no_strict: bool, @@ -666,6 +671,9 @@ pub struct RuntimeConfig { pub macro_task_rx: Receiver>, /// Import map for module resolution pub import_map: Option, + /// Optional per-iteration hook invoked inside the main run loop. See + /// [`PreTickHook`] for semantics and threading constraints. + pub pre_tick_hook: Option>, } pub struct Runtime { @@ -860,6 +868,12 @@ impl Runtime { self.host_hooks.drain_ready_timeout_jobs(); } + // Pump any extension-owned event loop (e.g. winit) before we + // consider blocking on macro tasks. + if let Some(ref hook) = self.config.pre_tick_hook { + hook(&self.host_hooks.host_data); + } + // Try to handle a macro task without blocking // This handles the case where a task completed so fast that the counter // was already decremented but the message is still in the channel diff --git a/crates/runtime/Cargo.toml b/crates/runtime/Cargo.toml index d9b5145..613d236 100644 --- a/crates/runtime/Cargo.toml +++ b/crates/runtime/Cargo.toml @@ -14,6 +14,7 @@ crypto = ["dep:ring", "dep:rand"] storage = ["dep:rusqlite"] virtualfs = ["storage"] serve = [] +window = ["dep:winit", "dep:raw-window-handle", "dep:wgpu"] hotpath = ["hotpath/hotpath", "andromeda-core/hotpath"] hotpath-alloc = ["hotpath/hotpath-alloc"] typescript = ["nova_vm/typescript"] @@ -64,6 +65,8 @@ libffi.workspace = true thiserror.workspace = true uuid.workspace = true wgpu = { workspace = true, optional = true } +winit = { workspace = true, optional = true } +raw-window-handle = { workspace = true, optional = true } rustls.workspace = true tokio-rustls.workspace = true webpki-roots.workspace = true diff --git a/crates/runtime/src/ext/canvas/mod.rs b/crates/runtime/src/ext/canvas/mod.rs index cb6a4d0..acf7af6 100644 --- a/crates/runtime/src/ext/canvas/mod.rs +++ b/crates/runtime/src/ext/canvas/mod.rs @@ -904,6 +904,7 @@ impl CanvasExt { .get_host_data() .downcast_ref::>() .unwrap(); + let (device, queue) = acquire_device_and_queue(host_data); let mut storage = host_data.storage.borrow_mut(); let res: &mut CanvasResources = storage.get_mut().unwrap(); // Create canvas data let canvas_rid = res.canvases.push(CanvasData { @@ -946,8 +947,8 @@ impl CanvasExt { direction: state::Direction::default(), }); - // Create renderer with GPU device. - let (device, queue) = create_wgpu_device_sync(); + // Renderer uses the wgpu device acquired above (shared with the + // window extension when both features are enabled). let dimensions = renderer::Dimensions { width, height }; let format = wgpu::TextureFormat::Bgra8Unorm; let renderer = renderer::Renderer::new(device, queue, format, dimensions); @@ -4782,6 +4783,59 @@ fn extract_image_region( result } +/// Flush pending canvas commands for the given canvas rid and return a +/// clone of its `resolve_target` texture. Clones are cheap — `wgpu::Texture` +/// is internally `Arc`-ed. Used by the `window` extension's canvas bridge +/// to sample the latest canvas frame in a blit render pass. Takes the raw +/// `OpsStorage` borrow to sidestep the `'gc` lifetime on `CanvasResources`. +#[cfg(feature = "window")] +pub(crate) fn render_canvas_to_texture( + storage: &mut OpsStorage, + canvas_rid: Rid, +) -> Option { + let res: &mut CanvasResources = storage.get_mut()?; + let mut renderer = res.renderers.get_mut(canvas_rid)?; + renderer.render_all(); + Some(renderer.resolve_target.clone()) +} + +/// Acquire a wgpu device+queue for a new canvas. When the `window` feature +/// is enabled the shared `WindowingGpu` is used so canvas textures live on +/// the same device as window surfaces — that's what lets `presentCanvas` +/// blit a canvas frame into a window without cross-device copies. If the +/// WindowingState slot is present but `ensure_gpu()` fails, we surface +/// that error as a panic rather than silently creating a divergent +/// standalone device — a mismatch would otherwise manifest as an opaque +/// wgpu validation error at the next `presentCanvas` call. When the +/// `window` feature is compiled out we fall back to stand-alone device +/// creation, which has always been the canvas default. +fn acquire_device_and_queue( + host_data: &HostData, +) -> (wgpu::Device, wgpu::Queue) { + #[cfg(feature = "window")] + { + let mut storage = host_data.storage.borrow_mut(); + if let Some(state) = storage.get_mut::() { + match state.ensure_gpu() { + Ok(gpu) => return (gpu.device.clone(), gpu.queue.clone()), + Err(e) => { + // Loud failure keeps the error close to the root cause. + // Silent fallback would produce a second, independent + // wgpu::Device; any later bridge call would then crash + // with a device-mismatch error with no backtrace to + // this moment. + panic!("[andromeda/canvas] shared WindowingGpu init failed: {e}"); + } + } + } + } + #[cfg(not(feature = "window"))] + { + let _ = host_data; // silence unused-parameter warning + } + create_wgpu_device_sync() +} + fn create_wgpu_device_sync() -> (wgpu::Device, wgpu::Queue) { let instance = wgpu::Instance::new(&wgpu::InstanceDescriptor { backends: wgpu::Backends::all(), diff --git a/crates/runtime/src/ext/canvas/mod.ts b/crates/runtime/src/ext/canvas/mod.ts index c6ac176..238db91 100644 --- a/crates/runtime/src/ext/canvas/mod.ts +++ b/crates/runtime/src/ext/canvas/mod.ts @@ -538,6 +538,15 @@ class OffscreenCanvas { this.#rid = __andromeda__.internal_canvas_create(width, height); } + /** + * Internal resource id — exposed so extensions like `Andromeda.Window` + * can bridge this canvas's rendered texture to another surface. Not + * part of the web OffscreenCanvas spec; treat as implementation detail. + */ + get rid(): number { + return this.#rid; + } + /** * Get the width of the canvas. */ diff --git a/crates/runtime/src/ext/canvas/renderer/render.rs b/crates/runtime/src/ext/canvas/renderer/render.rs index cb66c8b..4d29370 100644 --- a/crates/runtime/src/ext/canvas/renderer/render.rs +++ b/crates/runtime/src/ext/canvas/renderer/render.rs @@ -261,6 +261,15 @@ impl Renderer { view_formats: &[], }); + let srgb_view_format = match format { + wgpu::TextureFormat::Bgra8Unorm => Some(wgpu::TextureFormat::Bgra8UnormSrgb), + wgpu::TextureFormat::Rgba8Unorm => Some(wgpu::TextureFormat::Rgba8UnormSrgb), + _ => None, + }; + let resolve_view_formats: &[wgpu::TextureFormat] = match &srgb_view_format { + Some(f) => std::slice::from_ref(f), + None => &[], + }; let resolve_target = device.create_texture(&wgpu::TextureDescriptor { label: Some("Background (Resolve)"), dimension: wgpu::TextureDimension::D2, @@ -272,8 +281,10 @@ impl Renderer { height: dimensions.height, width: dimensions.width, }, - usage: wgpu::TextureUsages::RENDER_ATTACHMENT | wgpu::TextureUsages::COPY_SRC, - view_formats: &[], + usage: wgpu::TextureUsages::RENDER_ATTACHMENT + | wgpu::TextureUsages::COPY_SRC + | wgpu::TextureUsages::TEXTURE_BINDING, + view_formats: resolve_view_formats, }); // Stencil texture sample count must match the color attachment's @@ -411,6 +422,18 @@ impl Renderer { } self.queue.submit([encoder.finish()]); + + // Drain the command queue once submitted. Each `RenderCommand` + // owns a `wgpu::Buffer` + `wgpu::BindGroup`; retaining them across + // `render_all` calls made the canvas accumulate draw commands + // (and GPU buffers) indefinitely in per-frame rendering loops + // like `examples/breakout.ts` — linear slowdown + linear memory + // growth. Since the background attachment is cleared at the start + // of every pass, replaying prior-frame commands doesn't preserve + // any state that the next frame can't re-emit. All existing + // canvas examples call `render()`/`saveAsPng`/`toDataURL` once + // after drawing, so this change is behavior-preserving for them. + self.commands.clear(); } pub async fn create_bitmap(&mut self) -> Vec { diff --git a/crates/runtime/src/ext/mod.rs b/crates/runtime/src/ext/mod.rs index 08d8316..c7b4848 100644 --- a/crates/runtime/src/ext/mod.rs +++ b/crates/runtime/src/ext/mod.rs @@ -33,7 +33,8 @@ mod virtualfs; mod web; mod web_locks; mod webidl; - +#[cfg(feature = "window")] +pub mod window; pub use broadcast_channel::*; #[cfg(feature = "storage")] pub use cache_storage::*; @@ -65,3 +66,5 @@ pub use virtualfs::*; pub use web::*; pub use web_locks::*; pub use webidl::*; +#[cfg(feature = "window")] +pub use window::{WindowExt, pump_windowing_state}; diff --git a/crates/runtime/src/ext/window/blit.wgsl b/crates/runtime/src/ext/window/blit.wgsl new file mode 100644 index 0000000..efce912 --- /dev/null +++ b/crates/runtime/src/ext/window/blit.wgsl @@ -0,0 +1,22 @@ +struct VsOut { + @builtin(position) pos: vec4, + @location(0) uv: vec2, +}; + +@vertex +fn vs_main(@builtin(vertex_index) idx: u32) -> VsOut { + let x = f32((idx << 1u) & 2u); + let y = f32(idx & 2u); + var out: VsOut; + out.pos = vec4(x * 2.0 - 1.0, 1.0 - y * 2.0, 0.0, 1.0); + out.uv = vec2(x, y); + return out; +} + +@group(0) @binding(0) var src_texture: texture_2d; +@group(0) @binding(1) var src_sampler: sampler; + +@fragment +fn fs_main(in: VsOut) -> @location(0) vec4 { + return textureSample(src_texture, src_sampler, in.uv); +} diff --git a/crates/runtime/src/ext/window/canvas_bridge.rs b/crates/runtime/src/ext/window/canvas_bridge.rs new file mode 100644 index 0000000..15885c9 --- /dev/null +++ b/crates/runtime/src/ext/window/canvas_bridge.rs @@ -0,0 +1,175 @@ +// This Source Code Form is subject to the terms of the Mozilla Public +// License, v. 2.0. If a copy of the MPL was not distributed with this +// file, You can obtain one at https://mozilla.org/MPL/2.0/. + +use andromeda_core::{HostData, Rid}; + +use crate::RuntimeMacroTask; +use crate::ext::canvas::render_canvas_to_texture; + +use super::state::WindowingState; + +/// Present the latest frame of the canvas identified by `canvas_rid` into +/// the window identified by `win_rid`. Caller-side errors (closed window, +/// unknown canvas, GPU init failure) bubble back as `Err(String)` for the +/// op wrapper to convert into a JS exception. +pub fn present_canvas_on_window( + host_data: &HostData, + win_rid: u32, + canvas_rid_raw: u32, +) -> Result<(), String> { + use raw_window_handle::{HasDisplayHandle, HasWindowHandle}; + + let canvas_rid = Rid::from_index(canvas_rid_raw); + + let canvas_texture = { + let mut storage = host_data.storage.borrow_mut(); + render_canvas_to_texture(&mut storage, canvas_rid) + .ok_or_else(|| format!("canvas rid {canvas_rid_raw} not found"))? + }; + + let mut storage = host_data.storage.borrow_mut(); + let state: &mut WindowingState = storage + .get_mut() + .ok_or_else(|| "window extension not initialized".to_string())?; + + state.ensure_gpu()?; + state.ensure_blit()?; + + let data = state + .app + .windows + .get_mut(&win_rid) + .filter(|d| !d.closed) + .ok_or_else(|| "window has been closed".to_string())?; + + let gpu = state.gpu.as_mut().unwrap(); + + if data.surface.is_none() { + let target = wgpu::SurfaceTargetUnsafe::RawHandle { + raw_display_handle: data + .window + .display_handle() + .map_err(|e| format!("display_handle: {e}"))? + .as_raw(), + raw_window_handle: data + .window + .window_handle() + .map_err(|e| format!("window_handle: {e}"))? + .as_raw(), + }; + let surface = unsafe { gpu.instance.create_surface_unsafe(target) } + .map_err(|e| format!("create_surface: {e}"))?; + data.surface = Some(surface); + } + + let size = data.window.inner_size(); + let (width, height) = (size.width.max(1), size.height.max(1)); + + let needs_config = match &data.surface_config { + None => true, + Some(cfg) => cfg.width != width || cfg.height != height, + }; + if needs_config { + let surface = data.surface.as_ref().unwrap(); + let caps = surface.get_capabilities(&gpu.adapter); + let format = caps + .formats + .iter() + .copied() + .find(|f| f.is_srgb()) + .unwrap_or(caps.formats[0]); + let present_mode = if caps.present_modes.contains(&wgpu::PresentMode::Fifo) { + wgpu::PresentMode::Fifo + } else { + caps.present_modes[0] + }; + let alpha_mode = caps.alpha_modes[0]; + let cfg = wgpu::SurfaceConfiguration { + usage: wgpu::TextureUsages::RENDER_ATTACHMENT, + format, + width, + height, + present_mode, + desired_maximum_frame_latency: 2, + alpha_mode, + view_formats: vec![], + }; + surface.configure(&gpu.device, &cfg); + data.surface_config = Some(cfg); + } + + let surface_format = data.surface_config.as_ref().unwrap().format; + let frame = data + .surface + .as_ref() + .unwrap() + .get_current_texture() + .map_err(|e| format!("get_current_texture: {e}"))?; + let view = frame + .texture + .create_view(&wgpu::TextureViewDescriptor::default()); + + let canvas_view_format = match canvas_texture.format() { + wgpu::TextureFormat::Bgra8Unorm => Some(wgpu::TextureFormat::Bgra8UnormSrgb), + wgpu::TextureFormat::Rgba8Unorm => Some(wgpu::TextureFormat::Rgba8UnormSrgb), + _ => None, + }; + let canvas_view = canvas_texture.create_view(&wgpu::TextureViewDescriptor { + format: canvas_view_format, + ..Default::default() + }); + let blit = gpu.blit.as_mut().unwrap(); + let pipeline = blit + .pipeline_for_format(&gpu.device, surface_format) + .clone(); + let bind_group = gpu.device.create_bind_group(&wgpu::BindGroupDescriptor { + label: Some("andromeda-window-blit-bg"), + layout: &blit.bind_group_layout, + entries: &[ + wgpu::BindGroupEntry { + binding: 0, + resource: wgpu::BindingResource::TextureView(&canvas_view), + }, + wgpu::BindGroupEntry { + binding: 1, + resource: wgpu::BindingResource::Sampler(&blit.sampler), + }, + ], + }); + + let mut encoder = gpu + .device + .create_command_encoder(&wgpu::CommandEncoderDescriptor { + label: Some("andromeda-window-blit-encoder"), + }); + { + let mut pass = encoder.begin_render_pass(&wgpu::RenderPassDescriptor { + label: Some("andromeda-window-blit-pass"), + color_attachments: &[Some(wgpu::RenderPassColorAttachment { + view: &view, + resolve_target: None, + ops: wgpu::Operations { + load: wgpu::LoadOp::Clear(wgpu::Color { + r: 0.0, + g: 0.0, + b: 0.0, + a: 1.0, + }), + store: wgpu::StoreOp::Store, + }, + depth_slice: None, + })], + depth_stencil_attachment: None, + timestamp_writes: None, + occlusion_query_set: None, + }); + pass.set_pipeline(&pipeline); + pass.set_bind_group(0, &bind_group, &[]); + pass.draw(0..3, 0..1); + } + data.window.pre_present_notify(); + gpu.queue.submit(std::iter::once(encoder.finish())); + frame.present(); + Ok(()) +} diff --git a/crates/runtime/src/ext/window/events.rs b/crates/runtime/src/ext/window/events.rs new file mode 100644 index 0000000..b042df8 --- /dev/null +++ b/crates/runtime/src/ext/window/events.rs @@ -0,0 +1,90 @@ +// This Source Code Form is subject to the terms of the Mozilla Public +// License, v. 2.0. If a copy of the MPL was not distributed with this +// file, You can obtain one at https://mozilla.org/MPL/2.0/. + +use serde::Serialize; + +/// A single window event queued for delivery to JS on the next poll. +#[derive(Debug, Clone, Serialize)] +pub struct SerializedWindowEvent { + /// Target window rid. + pub rid: u32, + /// Event type name, intentionally DOM-aligned. + #[serde(rename = "type")] + pub kind: &'static str, + /// Event-specific payload — shape depends on `kind`. + pub detail: EventDetail, +} + +#[derive(Debug, Clone, Serialize)] +#[serde(untagged)] +pub enum EventDetail { + Empty {}, + Resize { + width: u32, + height: u32, + #[serde(rename = "scaleFactor")] + scale_factor: f64, + }, + Key { + key: String, + code: String, + /// Legacy numeric keyCode per the MDN table. See + /// https://developer.mozilla.org/en-US/docs/Web/API/KeyboardEvent/keyCode + #[serde(rename = "keyCode")] + key_code: u32, + /// Alias of `keyCode` — kept for compat with old code that reads + /// `event.which`. + which: u32, + /// 0 = standard, 1 = left modifier, 2 = right modifier, 3 = numpad. + location: u8, + #[serde(rename = "altKey")] + alt_key: bool, + #[serde(rename = "ctrlKey")] + ctrl_key: bool, + #[serde(rename = "metaKey")] + meta_key: bool, + #[serde(rename = "shiftKey")] + shift_key: bool, + repeat: bool, + /// Always `false` in this runtime since we don't have IME support, but included for completeness. + #[serde(rename = "isComposing")] + is_composing: bool, + }, + Mouse { + x: f64, + y: f64, + button: i32, + buttons: u32, + #[serde(rename = "altKey")] + alt_key: bool, + #[serde(rename = "ctrlKey")] + ctrl_key: bool, + #[serde(rename = "metaKey")] + meta_key: bool, + #[serde(rename = "shiftKey")] + shift_key: bool, + }, +} + +impl SerializedWindowEvent { + pub fn close(rid: u32) -> Self { + Self { + rid, + kind: "close", + detail: EventDetail::Empty {}, + } + } + + pub fn resize(rid: u32, width: u32, height: u32, scale_factor: f64) -> Self { + Self { + rid, + kind: "resize", + detail: EventDetail::Resize { + width, + height, + scale_factor, + }, + } + } +} diff --git a/crates/runtime/src/ext/window/keymap.rs b/crates/runtime/src/ext/window/keymap.rs new file mode 100644 index 0000000..6c382af --- /dev/null +++ b/crates/runtime/src/ext/window/keymap.rs @@ -0,0 +1,421 @@ +// This Source Code Form is subject to the terms of the Mozilla Public +// License, v. 2.0. If a copy of the MPL was not distributed with this +// file, You can obtain one at https://mozilla.org/MPL/2.0/. + +use winit::keyboard::{KeyCode, KeyLocation, NamedKey}; + +/// Map a winit `NamedKey` to the spec `KeyboardEvent.key` string. +pub fn named_key_to_spec(named: &NamedKey) -> &'static str { + match named { + NamedKey::Alt => "Alt", + NamedKey::AltGraph => "AltGraph", + NamedKey::CapsLock => "CapsLock", + NamedKey::Control => "Control", + NamedKey::Fn => "Fn", + NamedKey::FnLock => "FnLock", + NamedKey::Meta => "Meta", + NamedKey::NumLock => "NumLock", + NamedKey::ScrollLock => "ScrollLock", + NamedKey::Shift => "Shift", + NamedKey::Symbol => "Symbol", + NamedKey::SymbolLock => "SymbolLock", + NamedKey::Enter => "Enter", + NamedKey::Tab => "Tab", + NamedKey::Space => " ", + NamedKey::ArrowDown => "ArrowDown", + NamedKey::ArrowLeft => "ArrowLeft", + NamedKey::ArrowRight => "ArrowRight", + NamedKey::ArrowUp => "ArrowUp", + NamedKey::End => "End", + NamedKey::Home => "Home", + NamedKey::PageDown => "PageDown", + NamedKey::PageUp => "PageUp", + NamedKey::Backspace => "Backspace", + NamedKey::Clear => "Clear", + NamedKey::Copy => "Copy", + NamedKey::CrSel => "CrSel", + NamedKey::Cut => "Cut", + NamedKey::Delete => "Delete", + NamedKey::EraseEof => "EraseEof", + NamedKey::ExSel => "ExSel", + NamedKey::Insert => "Insert", + NamedKey::Paste => "Paste", + NamedKey::Redo => "Redo", + NamedKey::Undo => "Undo", + NamedKey::Accept => "Accept", + NamedKey::Again => "Again", + NamedKey::Attn => "Attn", + NamedKey::Cancel => "Cancel", + NamedKey::ContextMenu => "ContextMenu", + NamedKey::Escape => "Escape", + NamedKey::Execute => "Execute", + NamedKey::Find => "Find", + NamedKey::Help => "Help", + NamedKey::Pause => "Pause", + NamedKey::Play => "Play", + NamedKey::Props => "Props", + NamedKey::Select => "Select", + NamedKey::ZoomIn => "ZoomIn", + NamedKey::ZoomOut => "ZoomOut", + NamedKey::F1 => "F1", + NamedKey::F2 => "F2", + NamedKey::F3 => "F3", + NamedKey::F4 => "F4", + NamedKey::F5 => "F5", + NamedKey::F6 => "F6", + NamedKey::F7 => "F7", + NamedKey::F8 => "F8", + NamedKey::F9 => "F9", + NamedKey::F10 => "F10", + NamedKey::F11 => "F11", + NamedKey::F12 => "F12", + NamedKey::F13 => "F13", + NamedKey::F14 => "F14", + NamedKey::F15 => "F15", + NamedKey::F16 => "F16", + NamedKey::F17 => "F17", + NamedKey::F18 => "F18", + NamedKey::F19 => "F19", + NamedKey::F20 => "F20", + NamedKey::F21 => "F21", + NamedKey::F22 => "F22", + NamedKey::F23 => "F23", + NamedKey::F24 => "F24", + NamedKey::F25 => "F25", + NamedKey::F26 => "F26", + NamedKey::F27 => "F27", + NamedKey::F28 => "F28", + NamedKey::F29 => "F29", + NamedKey::F30 => "F30", + NamedKey::F31 => "F31", + NamedKey::F32 => "F32", + NamedKey::F33 => "F33", + NamedKey::F34 => "F34", + NamedKey::F35 => "F35", + NamedKey::BrowserBack => "BrowserBack", + NamedKey::BrowserFavorites => "BrowserFavorites", + NamedKey::BrowserForward => "BrowserForward", + NamedKey::BrowserHome => "BrowserHome", + NamedKey::BrowserRefresh => "BrowserRefresh", + NamedKey::BrowserSearch => "BrowserSearch", + NamedKey::BrowserStop => "BrowserStop", + NamedKey::MediaPlayPause => "MediaPlayPause", + NamedKey::MediaStop => "MediaStop", + NamedKey::MediaTrackNext => "MediaTrackNext", + NamedKey::MediaTrackPrevious => "MediaTrackPrevious", + NamedKey::AudioVolumeMute => "AudioVolumeMute", + NamedKey::AudioVolumeDown => "AudioVolumeDown", + NamedKey::AudioVolumeUp => "AudioVolumeUp", + NamedKey::LaunchApplication1 => "LaunchApplication1", + NamedKey::LaunchApplication2 => "LaunchApplication2", + NamedKey::LaunchMail => "LaunchMail", + _ => "Unidentified", + } +} + +/// Map a winit `KeyCode` to the spec `KeyboardEvent.code` string. +pub fn physical_key_to_code(code: KeyCode) -> &'static str { + match code { + KeyCode::KeyA => "KeyA", + KeyCode::KeyB => "KeyB", + KeyCode::KeyC => "KeyC", + KeyCode::KeyD => "KeyD", + KeyCode::KeyE => "KeyE", + KeyCode::KeyF => "KeyF", + KeyCode::KeyG => "KeyG", + KeyCode::KeyH => "KeyH", + KeyCode::KeyI => "KeyI", + KeyCode::KeyJ => "KeyJ", + KeyCode::KeyK => "KeyK", + KeyCode::KeyL => "KeyL", + KeyCode::KeyM => "KeyM", + KeyCode::KeyN => "KeyN", + KeyCode::KeyO => "KeyO", + KeyCode::KeyP => "KeyP", + KeyCode::KeyQ => "KeyQ", + KeyCode::KeyR => "KeyR", + KeyCode::KeyS => "KeyS", + KeyCode::KeyT => "KeyT", + KeyCode::KeyU => "KeyU", + KeyCode::KeyV => "KeyV", + KeyCode::KeyW => "KeyW", + KeyCode::KeyX => "KeyX", + KeyCode::KeyY => "KeyY", + KeyCode::KeyZ => "KeyZ", + KeyCode::Digit0 => "Digit0", + KeyCode::Digit1 => "Digit1", + KeyCode::Digit2 => "Digit2", + KeyCode::Digit3 => "Digit3", + KeyCode::Digit4 => "Digit4", + KeyCode::Digit5 => "Digit5", + KeyCode::Digit6 => "Digit6", + KeyCode::Digit7 => "Digit7", + KeyCode::Digit8 => "Digit8", + KeyCode::Digit9 => "Digit9", + KeyCode::ArrowDown => "ArrowDown", + KeyCode::ArrowLeft => "ArrowLeft", + KeyCode::ArrowRight => "ArrowRight", + KeyCode::ArrowUp => "ArrowUp", + KeyCode::Space => "Space", + KeyCode::Enter => "Enter", + KeyCode::Tab => "Tab", + KeyCode::Backspace => "Backspace", + KeyCode::Escape => "Escape", + KeyCode::CapsLock => "CapsLock", + KeyCode::Delete => "Delete", + KeyCode::Insert => "Insert", + KeyCode::Home => "Home", + KeyCode::End => "End", + KeyCode::PageDown => "PageDown", + KeyCode::PageUp => "PageUp", + KeyCode::ContextMenu => "ContextMenu", + KeyCode::PrintScreen => "PrintScreen", + KeyCode::ScrollLock => "ScrollLock", + KeyCode::Pause => "Pause", + KeyCode::ShiftLeft => "ShiftLeft", + KeyCode::ShiftRight => "ShiftRight", + KeyCode::ControlLeft => "ControlLeft", + KeyCode::ControlRight => "ControlRight", + KeyCode::AltLeft => "AltLeft", + KeyCode::AltRight => "AltRight", + KeyCode::SuperLeft => "MetaLeft", + KeyCode::SuperRight => "MetaRight", + KeyCode::Backquote => "Backquote", + KeyCode::Minus => "Minus", + KeyCode::Equal => "Equal", + KeyCode::BracketLeft => "BracketLeft", + KeyCode::BracketRight => "BracketRight", + KeyCode::Backslash => "Backslash", + KeyCode::Semicolon => "Semicolon", + KeyCode::Quote => "Quote", + KeyCode::Comma => "Comma", + KeyCode::Period => "Period", + KeyCode::Slash => "Slash", + KeyCode::IntlBackslash => "IntlBackslash", + KeyCode::IntlRo => "IntlRo", + KeyCode::IntlYen => "IntlYen", + KeyCode::F1 => "F1", + KeyCode::F2 => "F2", + KeyCode::F3 => "F3", + KeyCode::F4 => "F4", + KeyCode::F5 => "F5", + KeyCode::F6 => "F6", + KeyCode::F7 => "F7", + KeyCode::F8 => "F8", + KeyCode::F9 => "F9", + KeyCode::F10 => "F10", + KeyCode::F11 => "F11", + KeyCode::F12 => "F12", + KeyCode::F13 => "F13", + KeyCode::F14 => "F14", + KeyCode::F15 => "F15", + KeyCode::F16 => "F16", + KeyCode::F17 => "F17", + KeyCode::F18 => "F18", + KeyCode::F19 => "F19", + KeyCode::F20 => "F20", + KeyCode::F21 => "F21", + KeyCode::F22 => "F22", + KeyCode::F23 => "F23", + KeyCode::F24 => "F24", + KeyCode::F25 => "F25", + KeyCode::F26 => "F26", + KeyCode::F27 => "F27", + KeyCode::F28 => "F28", + KeyCode::F29 => "F29", + KeyCode::F30 => "F30", + KeyCode::F31 => "F31", + KeyCode::F32 => "F32", + KeyCode::F33 => "F33", + KeyCode::F34 => "F34", + KeyCode::F35 => "F35", + KeyCode::Numpad0 => "Numpad0", + KeyCode::Numpad1 => "Numpad1", + KeyCode::Numpad2 => "Numpad2", + KeyCode::Numpad3 => "Numpad3", + KeyCode::Numpad4 => "Numpad4", + KeyCode::Numpad5 => "Numpad5", + KeyCode::Numpad6 => "Numpad6", + KeyCode::Numpad7 => "Numpad7", + KeyCode::Numpad8 => "Numpad8", + KeyCode::Numpad9 => "Numpad9", + KeyCode::NumpadAdd => "NumpadAdd", + KeyCode::NumpadSubtract => "NumpadSubtract", + KeyCode::NumpadMultiply => "NumpadMultiply", + KeyCode::NumpadDivide => "NumpadDivide", + KeyCode::NumpadDecimal => "NumpadDecimal", + KeyCode::NumpadEnter => "NumpadEnter", + KeyCode::NumpadEqual => "NumpadEqual", + KeyCode::NumpadComma => "NumpadComma", + KeyCode::Lang1 => "Lang1", + KeyCode::Lang2 => "Lang2", + KeyCode::Lang3 => "Lang3", + KeyCode::Lang4 => "Lang4", + KeyCode::Lang5 => "Lang5", + KeyCode::Convert => "Convert", + KeyCode::NonConvert => "NonConvert", + KeyCode::KanaMode => "KanaMode", + KeyCode::BrowserBack => "BrowserBack", + KeyCode::BrowserFavorites => "BrowserFavorites", + KeyCode::BrowserForward => "BrowserForward", + KeyCode::BrowserHome => "BrowserHome", + KeyCode::BrowserRefresh => "BrowserRefresh", + KeyCode::BrowserSearch => "BrowserSearch", + KeyCode::BrowserStop => "BrowserStop", + KeyCode::MediaPlayPause => "MediaPlayPause", + KeyCode::MediaStop => "MediaStop", + KeyCode::MediaTrackNext => "MediaTrackNext", + KeyCode::MediaTrackPrevious => "MediaTrackPrevious", + KeyCode::MediaSelect => "MediaSelect", + KeyCode::Eject => "Eject", + KeyCode::AudioVolumeMute => "AudioVolumeMute", + KeyCode::AudioVolumeDown => "AudioVolumeDown", + KeyCode::AudioVolumeUp => "AudioVolumeUp", + KeyCode::LaunchApp1 => "LaunchApp1", + KeyCode::LaunchApp2 => "LaunchApp2", + KeyCode::LaunchMail => "LaunchMail", + KeyCode::Power => "Power", + KeyCode::Sleep => "Sleep", + KeyCode::WakeUp => "WakeUp", + KeyCode::Open => "Open", + KeyCode::Help => "Help", + KeyCode::NumLock => "NumLock", + KeyCode::Copy => "Copy", + KeyCode::Cut => "Cut", + KeyCode::Paste => "Paste", + KeyCode::Select => "Select", + KeyCode::Again => "Again", + KeyCode::Undo => "Undo", + KeyCode::Find => "Find", + KeyCode::Props => "Props", + _ => "Unidentified", + } +} + +/// Map a winit `KeyCode` to the deprecated numeric `KeyboardEvent.keyCode`. +pub fn physical_key_to_legacy_keycode(code: KeyCode) -> u32 { + match code { + KeyCode::Backspace => 8, + KeyCode::Tab => 9, + KeyCode::Enter | KeyCode::NumpadEnter => 13, + KeyCode::ShiftLeft | KeyCode::ShiftRight => 16, + KeyCode::ControlLeft | KeyCode::ControlRight => 17, + KeyCode::AltLeft | KeyCode::AltRight => 18, + KeyCode::Pause => 19, + KeyCode::CapsLock => 20, + KeyCode::Escape => 27, + KeyCode::Space => 32, + KeyCode::PageUp => 33, + KeyCode::PageDown => 34, + KeyCode::End => 35, + KeyCode::Home => 36, + KeyCode::ArrowLeft => 37, + KeyCode::ArrowUp => 38, + KeyCode::ArrowRight => 39, + KeyCode::ArrowDown => 40, + KeyCode::PrintScreen => 44, + KeyCode::Insert => 45, + KeyCode::Delete => 46, + KeyCode::Digit0 => 48, + KeyCode::Digit1 => 49, + KeyCode::Digit2 => 50, + KeyCode::Digit3 => 51, + KeyCode::Digit4 => 52, + KeyCode::Digit5 => 53, + KeyCode::Digit6 => 54, + KeyCode::Digit7 => 55, + KeyCode::Digit8 => 56, + KeyCode::Digit9 => 57, + KeyCode::KeyA => 65, + KeyCode::KeyB => 66, + KeyCode::KeyC => 67, + KeyCode::KeyD => 68, + KeyCode::KeyE => 69, + KeyCode::KeyF => 70, + KeyCode::KeyG => 71, + KeyCode::KeyH => 72, + KeyCode::KeyI => 73, + KeyCode::KeyJ => 74, + KeyCode::KeyK => 75, + KeyCode::KeyL => 76, + KeyCode::KeyM => 77, + KeyCode::KeyN => 78, + KeyCode::KeyO => 79, + KeyCode::KeyP => 80, + KeyCode::KeyQ => 81, + KeyCode::KeyR => 82, + KeyCode::KeyS => 83, + KeyCode::KeyT => 84, + KeyCode::KeyU => 85, + KeyCode::KeyV => 86, + KeyCode::KeyW => 87, + KeyCode::KeyX => 88, + KeyCode::KeyY => 89, + KeyCode::KeyZ => 90, + KeyCode::SuperLeft | KeyCode::SuperRight => 91, + KeyCode::ContextMenu => 93, + KeyCode::Numpad0 => 96, + KeyCode::Numpad1 => 97, + KeyCode::Numpad2 => 98, + KeyCode::Numpad3 => 99, + KeyCode::Numpad4 => 100, + KeyCode::Numpad5 => 101, + KeyCode::Numpad6 => 102, + KeyCode::Numpad7 => 103, + KeyCode::Numpad8 => 104, + KeyCode::Numpad9 => 105, + KeyCode::NumpadMultiply => 106, + KeyCode::NumpadAdd => 107, + KeyCode::NumpadSubtract => 109, + KeyCode::NumpadDecimal => 110, + KeyCode::NumpadDivide => 111, + KeyCode::F1 => 112, + KeyCode::F2 => 113, + KeyCode::F3 => 114, + KeyCode::F4 => 115, + KeyCode::F5 => 116, + KeyCode::F6 => 117, + KeyCode::F7 => 118, + KeyCode::F8 => 119, + KeyCode::F9 => 120, + KeyCode::F10 => 121, + KeyCode::F11 => 122, + KeyCode::F12 => 123, + KeyCode::F13 => 124, + KeyCode::F14 => 125, + KeyCode::F15 => 126, + KeyCode::F16 => 127, + KeyCode::F17 => 128, + KeyCode::F18 => 129, + KeyCode::F19 => 130, + KeyCode::F20 => 131, + KeyCode::F21 => 132, + KeyCode::F22 => 133, + KeyCode::F23 => 134, + KeyCode::F24 => 135, + KeyCode::NumLock => 144, + KeyCode::ScrollLock => 145, + KeyCode::Semicolon => 186, + KeyCode::Equal => 187, + KeyCode::Comma => 188, + KeyCode::Minus => 189, + KeyCode::Period => 190, + KeyCode::Slash => 191, + KeyCode::Backquote => 192, + KeyCode::BracketLeft => 219, + KeyCode::Backslash => 220, + KeyCode::BracketRight => 221, + KeyCode::Quote => 222, + _ => 0, + } +} + +/// Map a winit `KeyLocation` to the `KeyboardEvent.location` numeric constant. +pub fn location_to_u8(loc: KeyLocation) -> u8 { + match loc { + KeyLocation::Standard => 0, + KeyLocation::Left => 1, + KeyLocation::Right => 2, + KeyLocation::Numpad => 3, + } +} diff --git a/crates/runtime/src/ext/window/mod.rs b/crates/runtime/src/ext/window/mod.rs new file mode 100644 index 0000000..94c5115 --- /dev/null +++ b/crates/runtime/src/ext/window/mod.rs @@ -0,0 +1,607 @@ +// This Source Code Form is subject to the terms of the Mozilla Public +// License, v. 2.0. If a copy of the MPL was not distributed with this +// file, You can obtain one at https://mozilla.org/MPL/2.0/. + +use std::time::Duration; + +use andromeda_core::{Extension, ExtensionOp, HostData, OpsStorage}; +use nova_vm::{ + ecmascript::{Agent, ArgumentsList, ExceptionType, JsResult, Value}, + engine::{Bindable, GcScope}, +}; + +use crate::RuntimeMacroTask; + +#[cfg(feature = "canvas")] +pub mod canvas_bridge; +pub mod events; +pub mod keymap; +pub mod state; + +use state::{CreateWindowOptions, PendingCreation, WindowingState}; + +/// The windowing extension. +#[derive(Default)] +pub struct WindowExt; + +impl WindowExt { + pub fn new_extension() -> Extension { + Extension { + name: "window", + ops: vec![ + ExtensionOp::new( + "internal_window_create", + Self::internal_window_create, + 1, + false, + ), + ExtensionOp::new( + "internal_window_close", + Self::internal_window_close, + 1, + false, + ), + ExtensionOp::new( + "internal_window_poll_events", + Self::internal_window_poll_events, + 0, + false, + ), + ExtensionOp::new( + "internal_window_raw_handle", + Self::internal_window_raw_handle, + 1, + false, + ), + ExtensionOp::new( + "internal_window_set_title", + Self::internal_window_set_title, + 2, + false, + ), + ExtensionOp::new( + "internal_window_get_size", + Self::internal_window_get_size, + 1, + false, + ), + ExtensionOp::new( + "internal_window_set_size", + Self::internal_window_set_size, + 3, + false, + ), + ExtensionOp::new( + "internal_window_set_visible", + Self::internal_window_set_visible, + 2, + false, + ), + ExtensionOp::new( + "internal_window_present_color", + Self::internal_window_present_color, + 5, + false, + ), + #[cfg(feature = "canvas")] + ExtensionOp::new( + "internal_window_present_canvas", + Self::internal_window_present_canvas, + 2, + false, + ), + ], + storage: Some(Box::new(|storage: &mut OpsStorage| { + storage.insert(WindowingState::default()); + })), + files: vec![include_str!("./mod.ts")], + } + } + + /// `internal_window_create(optionsJson)` -> rid as string. + fn internal_window_create<'gc>( + agent: &mut Agent, + _this: Value, + args: ArgumentsList, + mut gc: GcScope<'gc, '_>, + ) -> JsResult<'gc, Value<'gc>> { + let opts_binding = args.get(0).to_string(agent, gc.reborrow()).unbind()?; + let opts_str = opts_binding + .as_str(agent) + .expect("String is not valid UTF-8") + .to_string(); + + let options = match CreateWindowOptions::from_json(&opts_str) { + Ok(o) => o, + Err(e) => { + return throw_window_err(agent, "createWindow", &e, gc); + } + }; + + let host_data = agent + .get_host_data() + .downcast_ref::>() + .unwrap(); + + let rid = { + let mut storage = host_data.storage.borrow_mut(); + let state: &mut WindowingState = storage.get_mut().unwrap(); + if let Err(e) = state.ensure_event_loop() { + drop(storage); + return throw_window_err(agent, "createWindow", &e, gc); + } + let rid = state.app.reserve_rid(); + state + .app + .pending_creations + .push(PendingCreation { rid, options }); + rid + }; + + let deadline = std::time::Instant::now() + Duration::from_millis(1500); + loop { + { + let mut storage = host_data.storage.borrow_mut(); + let state: &mut WindowingState = storage.get_mut().unwrap(); + state.pump(Duration::from_millis(10)); + if state.app.windows.contains_key(&rid) { + break; + } + } + if std::time::Instant::now() >= deadline { + return throw_window_err( + agent, + "createWindow", + "window creation did not complete within 1500ms (event loop did not reach resumed state)", + gc, + ); + } + } + + Ok(Value::from_string(agent, rid.to_string(), gc.nogc()).unbind()) + } + + /// `internal_window_close(rid)` -> undefined. Idempotent. + fn internal_window_close<'gc>( + agent: &mut Agent, + _this: Value, + args: ArgumentsList, + mut gc: GcScope<'gc, '_>, + ) -> JsResult<'gc, Value<'gc>> { + let rid = args.get(0).to_int32(agent, gc.reborrow()).unbind()? as u32; + let host_data = agent + .get_host_data() + .downcast_ref::>() + .unwrap(); + let mut storage = host_data.storage.borrow_mut(); + let state: &mut WindowingState = storage.get_mut().unwrap(); + state.app.remove(rid); + Ok(Value::Undefined) + } + + /// `internal_window_poll_events()` -> JSON array string of pending events. + fn internal_window_poll_events<'gc>( + agent: &mut Agent, + _this: Value, + _args: ArgumentsList, + gc: GcScope<'gc, '_>, + ) -> JsResult<'gc, Value<'gc>> { + let host_data = agent + .get_host_data() + .downcast_ref::>() + .unwrap(); + let json = { + let mut storage = host_data.storage.borrow_mut(); + let state: &mut WindowingState = storage.get_mut().unwrap(); + state.pump(Duration::ZERO); + let drained: Vec<_> = state.app.pending_events.drain(..).collect(); + serde_json::to_string(&drained).unwrap_or_else(|_| "[]".to_string()) + }; + Ok(Value::from_string(agent, json, gc.nogc()).unbind()) + } +} + +impl WindowExt { + /// `internal_window_raw_handle(rid)` -> JSON string: + fn internal_window_raw_handle<'gc>( + agent: &mut Agent, + _this: Value, + args: ArgumentsList, + mut gc: GcScope<'gc, '_>, + ) -> JsResult<'gc, Value<'gc>> { + let rid = args.get(0).to_int32(agent, gc.reborrow()).unbind()? as u32; + let host_data = agent + .get_host_data() + .downcast_ref::>() + .unwrap(); + let json = { + let storage = host_data.storage.borrow(); + let state: &WindowingState = storage.get().unwrap(); + let data = match state.app.get(rid) { + Some(d) if !d.closed => d, + _ => { + drop(storage); + return throw_window_err(agent, "rawHandle", "window has been closed", gc); + } + }; + match raw_handle_json(&data.window) { + Ok(j) => j, + Err(e) => { + drop(storage); + return throw_window_err(agent, "rawHandle", &e, gc); + } + } + }; + Ok(Value::from_string(agent, json, gc.nogc()).unbind()) + } + + fn internal_window_set_title<'gc>( + agent: &mut Agent, + _this: Value, + args: ArgumentsList, + mut gc: GcScope<'gc, '_>, + ) -> JsResult<'gc, Value<'gc>> { + let rid = args.get(0).to_int32(agent, gc.reborrow()).unbind()? as u32; + let title_binding = args.get(1).to_string(agent, gc.reborrow()).unbind()?; + let title = title_binding + .as_str(agent) + .expect("String is not valid UTF-8") + .to_string(); + let host_data = agent + .get_host_data() + .downcast_ref::>() + .unwrap(); + let mut storage = host_data.storage.borrow_mut(); + let state: &mut WindowingState = storage.get_mut().unwrap(); + match state.app.get_mut(rid) { + Some(d) if !d.closed => { + d.window.set_title(&title); + Ok(Value::Undefined) + } + _ => { + drop(storage); + throw_window_err(agent, "setTitle", "window has been closed", gc) + } + } + } + + fn internal_window_get_size<'gc>( + agent: &mut Agent, + _this: Value, + args: ArgumentsList, + mut gc: GcScope<'gc, '_>, + ) -> JsResult<'gc, Value<'gc>> { + let rid = args.get(0).to_int32(agent, gc.reborrow()).unbind()? as u32; + let host_data = agent + .get_host_data() + .downcast_ref::>() + .unwrap(); + let json = { + let storage = host_data.storage.borrow(); + let state: &WindowingState = storage.get().unwrap(); + match state.app.get(rid) { + Some(d) if !d.closed => { + let size = d.window.inner_size(); + let scale = d.window.scale_factor(); + format!( + "{{\"width\":{},\"height\":{},\"scaleFactor\":{}}}", + size.width, size.height, scale + ) + } + _ => { + drop(storage); + return throw_window_err(agent, "getSize", "window has been closed", gc); + } + } + }; + Ok(Value::from_string(agent, json, gc.nogc()).unbind()) + } + + fn internal_window_set_size<'gc>( + agent: &mut Agent, + _this: Value, + args: ArgumentsList, + mut gc: GcScope<'gc, '_>, + ) -> JsResult<'gc, Value<'gc>> { + let rid = args.get(0).to_int32(agent, gc.reborrow()).unbind()? as u32; + let width = args.get(1).to_int32(agent, gc.reborrow()).unbind()? as u32; + let height = args.get(2).to_int32(agent, gc.reborrow()).unbind()? as u32; + if width == 0 || height == 0 { + return throw_window_err(agent, "setSize", "width and height must be positive", gc); + } + let host_data = agent + .get_host_data() + .downcast_ref::>() + .unwrap(); + let mut storage = host_data.storage.borrow_mut(); + let state: &mut WindowingState = storage.get_mut().unwrap(); + match state.app.get_mut(rid) { + Some(d) if !d.closed => { + let _ = d + .window + .request_inner_size(winit::dpi::LogicalSize::new(width, height)); + Ok(Value::Undefined) + } + _ => { + drop(storage); + throw_window_err(agent, "setSize", "window has been closed", gc) + } + } + } + + /// Present a solid-color frame into the window's swapchain. + fn internal_window_present_color<'gc>( + agent: &mut Agent, + _this: Value, + args: ArgumentsList, + mut gc: GcScope<'gc, '_>, + ) -> JsResult<'gc, Value<'gc>> { + let rid = args.get(0).to_int32(agent, gc.reborrow()).unbind()? as u32; + let r = args + .get(1) + .to_number(agent, gc.reborrow()) + .unbind()? + .into_f64(agent); + let g = args + .get(2) + .to_number(agent, gc.reborrow()) + .unbind()? + .into_f64(agent); + let b = args + .get(3) + .to_number(agent, gc.reborrow()) + .unbind()? + .into_f64(agent); + let a = args + .get(4) + .to_number(agent, gc.reborrow()) + .unbind()? + .into_f64(agent); + let host_data = agent + .get_host_data() + .downcast_ref::>() + .unwrap(); + let mut storage = host_data.storage.borrow_mut(); + let state: &mut WindowingState = storage.get_mut().unwrap(); + if let Err(e) = state.ensure_gpu() { + drop(storage); + return throw_window_err(agent, "present", &e, gc); + } + match present_color_on_window(state, rid, r as f32, g as f32, b as f32, a as f32) { + Ok(()) => Ok(Value::Undefined), + Err(e) => { + drop(storage); + throw_window_err(agent, "present", &e, gc) + } + } + } + + /// Present the latest frame of an `OffscreenCanvas` into a window's + /// swapchain. + #[cfg(feature = "canvas")] + fn internal_window_present_canvas<'gc>( + agent: &mut Agent, + _this: Value, + args: ArgumentsList, + mut gc: GcScope<'gc, '_>, + ) -> JsResult<'gc, Value<'gc>> { + let win_rid = args.get(0).to_int32(agent, gc.reborrow()).unbind()? as u32; + let canvas_rid = args.get(1).to_int32(agent, gc.reborrow()).unbind()? as u32; + let host_data = agent + .get_host_data() + .downcast_ref::>() + .unwrap(); + match canvas_bridge::present_canvas_on_window(host_data, win_rid, canvas_rid) { + Ok(()) => Ok(Value::Undefined), + Err(e) => throw_window_err(agent, "presentCanvas", &e, gc), + } + } + + fn internal_window_set_visible<'gc>( + agent: &mut Agent, + _this: Value, + args: ArgumentsList, + mut gc: GcScope<'gc, '_>, + ) -> JsResult<'gc, Value<'gc>> { + let rid = args.get(0).to_int32(agent, gc.reborrow()).unbind()? as u32; + // Match standard JS boolean coercion for the few value kinds that + // can reach here. The TS shim always passes `!!visible`, but the + // op is the stable contract and should not silently invert truthy + // non-Boolean inputs. + let visible = match args.get(1).unbind() { + Value::Boolean(b) => b, + Value::Undefined | Value::Null => false, + Value::Integer(i) => i.into_i64() != 0, + _ => true, + }; + let host_data = agent + .get_host_data() + .downcast_ref::>() + .unwrap(); + let mut storage = host_data.storage.borrow_mut(); + let state: &mut WindowingState = storage.get_mut().unwrap(); + match state.app.get_mut(rid) { + Some(d) if !d.closed => { + d.window.set_visible(visible); + Ok(Value::Undefined) + } + _ => { + drop(storage); + throw_window_err(agent, "setVisible", "window has been closed", gc) + } + } + } +} + +fn raw_handle_json(window: &winit::window::Window) -> Result { + use raw_window_handle::{HasDisplayHandle, HasWindowHandle, RawDisplayHandle, RawWindowHandle}; + + let wh = window + .window_handle() + .map_err(|e| format!("window_handle unavailable: {e}"))?; + let dh = window + .display_handle() + .map_err(|e| format!("display_handle unavailable: {e}"))?; + let size = window.inner_size(); + + let (system, window_value) = match wh.as_raw() { + RawWindowHandle::AppKit(h) => ("cocoa", h.ns_view.as_ptr() as u64), + RawWindowHandle::Win32(h) => ("win32", h.hwnd.get() as u64), + #[allow(clippy::useless_conversion)] + RawWindowHandle::Xlib(h) => ("x11", u64::from(h.window)), + #[allow(clippy::useless_conversion)] + RawWindowHandle::Xcb(h) => ("x11", u64::from(h.window.get())), + RawWindowHandle::Wayland(h) => ("wayland", h.surface.as_ptr() as u64), + other => { + return Err(format!("unsupported window handle variant: {other:?}")); + } + }; + + let display_value: u64 = match dh.as_raw() { + RawDisplayHandle::AppKit(_) => 0, + RawDisplayHandle::Xlib(h) => h.display.map_or(0, |p| p.as_ptr() as u64), + RawDisplayHandle::Xcb(h) => h.connection.map_or(0, |p| p.as_ptr() as u64), + RawDisplayHandle::Wayland(h) => h.display.as_ptr() as u64, + // Win32 / UiKit / Orbital / Ohos etc. don't expose a separate + // display pointer — 0 is the accepted sentinel. + _ => 0, + }; + + Ok(format!( + "{{\"system\":\"{system}\",\"windowHandle\":\"{window_value}\",\"displayHandle\":\"{display_value}\",\"width\":{},\"height\":{}}}", + size.width, size.height + )) +} + +/// Pre-tick hook invoked once per iteration of `Runtime::run`. Drives the +/// winit event loop so window events flow even when JS is idle. +pub fn pump_windowing_state(host_data: &HostData) { + let mut storage = host_data.storage.borrow_mut(); + if let Some(state) = storage.get_mut::() { + state.pump(Duration::ZERO); + } +} + +/// Ensure the window has a configured surface that matches its current size, +/// then clear to the given RGBA and present. Separated from the op body so +/// the mutable-borrow juggling stays readable. +fn present_color_on_window( + state: &mut WindowingState, + rid: u32, + r: f32, + g: f32, + b: f32, + a: f32, +) -> Result<(), String> { + use raw_window_handle::{HasDisplayHandle, HasWindowHandle}; + let gpu = state.gpu.as_ref().ok_or("GPU not initialized")?; + let data = match state.app.windows.get_mut(&rid) { + Some(d) if !d.closed => d, + _ => return Err("window has been closed".to_string()), + }; + + // Create the surface on first call. + if data.surface.is_none() { + let target = wgpu::SurfaceTargetUnsafe::RawHandle { + raw_display_handle: data + .window + .display_handle() + .map_err(|e| format!("display_handle: {e}"))? + .as_raw(), + raw_window_handle: data + .window + .window_handle() + .map_err(|e| format!("window_handle: {e}"))? + .as_raw(), + }; + let surface = unsafe { gpu.instance.create_surface_unsafe(target) } + .map_err(|e| format!("create_surface: {e}"))?; + data.surface = Some(surface); + } + + let size = data.window.inner_size(); + let (width, height) = (size.width.max(1), size.height.max(1)); + + let needs_config = match &data.surface_config { + None => true, + Some(cfg) => cfg.width != width || cfg.height != height, + }; + if needs_config { + let surface = data.surface.as_ref().unwrap(); + let caps = surface.get_capabilities(&gpu.adapter); + let format = caps + .formats + .iter() + .copied() + .find(|f| f.is_srgb()) + .unwrap_or(caps.formats[0]); + let present_mode = if caps.present_modes.contains(&wgpu::PresentMode::Fifo) { + wgpu::PresentMode::Fifo + } else { + caps.present_modes[0] + }; + let alpha_mode = caps.alpha_modes[0]; + let cfg = wgpu::SurfaceConfiguration { + usage: wgpu::TextureUsages::RENDER_ATTACHMENT, + format, + width, + height, + present_mode, + desired_maximum_frame_latency: 2, + alpha_mode, + view_formats: vec![], + }; + surface.configure(&gpu.device, &cfg); + data.surface_config = Some(cfg); + } + + let surface = data.surface.as_ref().unwrap(); + let frame = surface + .get_current_texture() + .map_err(|e| format!("get_current_texture: {e}"))?; + let view = frame + .texture + .create_view(&wgpu::TextureViewDescriptor::default()); + let mut encoder = gpu + .device + .create_command_encoder(&wgpu::CommandEncoderDescriptor { + label: Some("andromeda-window-present"), + }); + { + let _pass = encoder.begin_render_pass(&wgpu::RenderPassDescriptor { + label: Some("andromeda-window-clear"), + color_attachments: &[Some(wgpu::RenderPassColorAttachment { + view: &view, + resolve_target: None, + ops: wgpu::Operations { + load: wgpu::LoadOp::Clear(wgpu::Color { + r: r as f64, + g: g as f64, + b: b as f64, + a: a as f64, + }), + store: wgpu::StoreOp::Store, + }, + depth_slice: None, + })], + depth_stencil_attachment: None, + timestamp_writes: None, + occlusion_query_set: None, + }); + } + data.window.pre_present_notify(); + gpu.queue.submit(std::iter::once(encoder.finish())); + frame.present(); + Ok(()) +} + +fn throw_window_err<'gc>( + agent: &mut Agent, + operation: &str, + message: &str, + gc: GcScope<'gc, '_>, +) -> JsResult<'gc, Value<'gc>> { + let full = format!("window.{operation}: {message}"); + let err = agent.throw_exception(ExceptionType::Error, full, gc.nogc()); + Err(err.unbind()) +} diff --git a/crates/runtime/src/ext/window/mod.ts b/crates/runtime/src/ext/window/mod.ts new file mode 100644 index 0000000..202e7f5 --- /dev/null +++ b/crates/runtime/src/ext/window/mod.ts @@ -0,0 +1,281 @@ +// This Source Code Form is subject to the terms of the Mozilla Public +// License, v. 2.0. If a copy of the MPL was not distributed with this +// file, You can obtain one at https://mozilla.org/MPL/2.0/. + +// deno-lint-ignore-file no-explicit-any + +const __andromeda_window_registry: Map = new Map(); + +interface AndromedaWindowEvent { + type: string; + detail: any; + target: AndromedaWindow; +} + +type AndromedaWindowListener = (event: AndromedaWindowEvent) => void; + +class AndromedaWindow { + #rid: number; + #closed = false; + #title: string; + #width: number; + #height: number; + #listeners: Map> = new Map(); + + constructor( + options: { + title?: string; + width?: number; + height?: number; + resizable?: boolean; + visible?: boolean; + } = {}, + ) { + const title = options.title ?? "Andromeda"; + const width = options.width ?? 800; + const height = options.height ?? 600; + const payload = JSON.stringify({ + title, + width, + height, + resizable: options.resizable ?? true, + visible: options.visible ?? true, + }); + // @ts-ignore - internal op surface + const ridStr = (globalThis as any).__andromeda__.internal_window_create( + payload, + ); + this.#rid = Number(ridStr); + this.#title = title; + this.#width = width; + this.#height = height; + __andromeda_window_registry.set(this.#rid, this); + try { + // @ts-ignore - internal op surface + const rawSize = ( + globalThis as any + ).__andromeda__.internal_window_get_size(this.#rid); + const parsed = JSON.parse(rawSize); + this.#width = parsed.width; + this.#height = parsed.height; + } catch { + // Leave the requested values as a best-effort fallback. + } + } + + addEventListener(type: string, listener: AndromedaWindowListener): void { + if (typeof listener !== "function") return; + let set = this.#listeners.get(type); + if (!set) { + set = new Set(); + this.#listeners.set(type, set); + } + set.add(listener); + } + + removeEventListener(type: string, listener: AndromedaWindowListener): void { + const set = this.#listeners.get(type); + if (set) set.delete(listener); + } + + dispatchEvent(event: AndromedaWindowEvent): boolean { + const set = this.#listeners.get(event.type); + if (!set) return true; + for (const listener of Array.from(set)) { + try { + listener.call(this, event); + } catch (err) { + console.error("[Andromeda.Window] listener threw:", err); + } + } + return true; + } + + get rid(): number { + return this.#rid; + } + + get title(): string { + return this.#title; + } + + get width(): number { + return this.#width; + } + + get height(): number { + return this.#height; + } + + get closed(): boolean { + return this.#closed; + } + + close(): void { + if (this.#closed) return; + this.#closed = true; + __andromeda_window_registry.delete(this.#rid); + // @ts-ignore - internal op surface + (globalThis as any).__andromeda__.internal_window_close(this.#rid); + } + + /** + * Return the native window + display handles along with current size. The + * shape matches `Deno.UnsafeWindowSurface`'s input so a future WebGPU + * surface bridge can pass it through verbatim. Pointer-sized values come + * back as strings; cast to `BigInt` on the caller side if needed. + */ + rawHandle(): { + system: "cocoa" | "win32" | "x11" | "wayland"; + windowHandle: string; + displayHandle: string; + width: number; + height: number; + } { + // @ts-ignore - internal op surface + const raw = (globalThis as any).__andromeda__.internal_window_raw_handle( + this.#rid, + ); + return JSON.parse(raw); + } + + setTitle(title: string): void { + // @ts-ignore - internal op surface + (globalThis as any).__andromeda__.internal_window_set_title( + this.#rid, + String(title), + ); + this.#title = String(title); + } + + getSize(): { width: number; height: number; scaleFactor: number } { + // @ts-ignore - internal op surface + const raw = (globalThis as any).__andromeda__.internal_window_get_size( + this.#rid, + ); + const parsed = JSON.parse(raw); + this.#width = parsed.width; + this.#height = parsed.height; + return parsed; + } + + setSize(width: number, height: number): void { + // @ts-ignore - internal op surface + (globalThis as any).__andromeda__.internal_window_set_size( + this.#rid, + width | 0, + height | 0, + ); + this.#width = width | 0; + this.#height = height | 0; + } + + setVisible(visible: boolean): void { + // @ts-ignore - internal op surface + (globalThis as any).__andromeda__.internal_window_set_visible( + this.#rid, + !!visible, + ); + } + + /** + * Clear the window to a solid RGBA color and present. Channel values are + * in the 0..1 range. Lazily initializes the shared wgpu context and the + * window's surface on first call. + */ + present(r: number, g: number, b: number, a: number = 1): void { + // @ts-ignore - internal op surface + (globalThis as any).__andromeda__.internal_window_present_color( + this.#rid, + Number(r), + Number(g), + Number(b), + Number(a), + ); + } + + /** + * Flush any pending draw commands on the given canvas and present its + * latest frame into this window's swapchain. Requires the runtime to + * be built with both the `window` and `canvas` features enabled (the + * op is absent otherwise and this method throws a clear error). + */ + presentCanvas(canvas: { rid: number }): void { + // @ts-ignore - internal op surface + const op = (globalThis as any).__andromeda__.internal_window_present_canvas; + if (typeof op !== "function") { + throw new Error( + "presentCanvas requires both the `window` and `canvas` features to be enabled.", + ); + } + if ( + !canvas || + typeof canvas.rid !== "number" || + !Number.isInteger(canvas.rid) || + canvas.rid < 0 + ) { + throw new TypeError( + "presentCanvas: expected an object with a non-negative integer `rid` field", + ); + } + op(this.#rid, canvas.rid); + } + + // Internal — used by mainloop to patch cached dimensions after a resize. + _updateSize(width: number, height: number): void { + this.#width = width; + this.#height = height; + } +} + +function __andromeda_window_poll(): Array<{ + rid: number; + type: string; + detail: any; +}> { + // @ts-ignore - internal op surface + const raw = (globalThis as any).__andromeda__.internal_window_poll_events(); + if (!raw) return []; + try { + return JSON.parse(raw); + } catch { + return []; + } +} + +async function __andromeda_window_mainloop( + callback?: () => void | Promise, +): Promise { + // Run until every registered window is closed. If the user's callback + // throws, close every open window before propagating so windows don't + // stay visible on screen after an unhandled rejection. + try { + while (__andromeda_window_registry.size > 0) { + const events = __andromeda_window_poll(); + for (const e of events) { + const target = __andromeda_window_registry.get(e.rid); + if (!target) continue; + if (e.type === "resize" && e.detail) { + target._updateSize(e.detail.width, e.detail.height); + } + target.dispatchEvent({ type: e.type, detail: e.detail, target }); + if (e.type === "close") { + target.close(); + } + } + if (callback) { + await callback(); + } + // Yield to the event loop so timers/promises can run. + await new Promise((r) => setTimeout(r, 0)); + } + } finally { + for (const w of Array.from(__andromeda_window_registry.values())) { + w.close(); + } + } +} +// @ts-ignore - cross-file handoff +(globalThis as any).__andromeda_window_class = AndromedaWindow; +// @ts-ignore - cross-file handoff +(globalThis as any).__andromeda_window_mainloop = __andromeda_window_mainloop; diff --git a/crates/runtime/src/ext/window/state.rs b/crates/runtime/src/ext/window/state.rs new file mode 100644 index 0000000..7de5696 --- /dev/null +++ b/crates/runtime/src/ext/window/state.rs @@ -0,0 +1,506 @@ +// This Source Code Form is subject to the terms of the Mozilla Public +// License, v. 2.0. If a copy of the MPL was not distributed with this +// file, You can obtain one at https://mozilla.org/MPL/2.0/. + +use std::collections::{HashMap, VecDeque}; +use std::rc::Rc; +use std::time::Duration; + +use winit::application::ApplicationHandler; +use winit::event::{ElementState, KeyEvent, MouseButton, WindowEvent}; +use winit::event_loop::{ActiveEventLoop, EventLoop}; +use winit::keyboard::{ModifiersState, PhysicalKey}; +use winit::platform::pump_events::EventLoopExtPumpEvents; +use winit::window::{Window, WindowAttributes, WindowId}; + +use super::events::{EventDetail, SerializedWindowEvent}; + +/// Options accepted by `createWindow(options)`, parsed from JSON on entry. +#[derive(Debug, Clone)] +pub struct CreateWindowOptions { + pub title: String, + pub width: u32, + pub height: u32, + pub resizable: bool, + pub visible: bool, +} + +impl CreateWindowOptions { + pub fn from_json(raw: &str) -> Result { + let value: serde_json::Value = + serde_json::from_str(raw).map_err(|e| format!("invalid options JSON: {e}"))?; + let title = value + .get("title") + .and_then(|v| v.as_str()) + .unwrap_or("Andromeda") + .to_string(); + let width = value.get("width").and_then(|v| v.as_u64()).unwrap_or(800) as u32; + let height = value.get("height").and_then(|v| v.as_u64()).unwrap_or(600) as u32; + if width == 0 || height == 0 { + return Err("width and height must be positive".to_string()); + } + let resizable = value + .get("resizable") + .and_then(|v| v.as_bool()) + .unwrap_or(true); + let visible = value + .get("visible") + .and_then(|v| v.as_bool()) + .unwrap_or(true); + Ok(Self { + title, + width, + height, + resizable, + visible, + }) + } +} + +/// Per-window bookkeeping stored in the `WindowingApp`. +pub struct WindowData { + pub window: Rc, + pub closed: bool, + pub surface: Option>, + pub surface_config: Option, +} + +/// Shared GPU context — one instance/adapter/device/queue for all windows. +/// Lazily initialized on first `present()`. +pub struct WindowingGpu { + pub instance: wgpu::Instance, + pub adapter: wgpu::Adapter, + pub device: wgpu::Device, + pub queue: wgpu::Queue, + /// Blit shader + sampler + per-format pipeline cache for + /// `presentCanvas`. Populated lazily on first canvas-present call. + pub blit: Option, +} + +/// Shader module, bind-group layout, sampler, and cached render pipelines +/// for the canvas → window surface blit. One entry per destination format +/// (typically just one in practice — the window's swapchain format). +pub struct BlitResources { + pub shader: wgpu::ShaderModule, + pub sampler: wgpu::Sampler, + pub bind_group_layout: wgpu::BindGroupLayout, + pub pipeline_layout: wgpu::PipelineLayout, + pub pipelines: std::collections::HashMap, +} + +impl BlitResources { + pub fn new(device: &wgpu::Device) -> Self { + let shader = device.create_shader_module(wgpu::ShaderModuleDescriptor { + label: Some("andromeda-window-blit-shader"), + source: wgpu::ShaderSource::Wgsl(include_str!("./blit.wgsl").into()), + }); + let sampler = device.create_sampler(&wgpu::SamplerDescriptor { + label: Some("andromeda-window-blit-sampler"), + mag_filter: wgpu::FilterMode::Linear, + min_filter: wgpu::FilterMode::Linear, + mipmap_filter: wgpu::FilterMode::Nearest, + ..Default::default() + }); + let bind_group_layout = device.create_bind_group_layout(&wgpu::BindGroupLayoutDescriptor { + label: Some("andromeda-window-blit-bgl"), + entries: &[ + wgpu::BindGroupLayoutEntry { + binding: 0, + visibility: wgpu::ShaderStages::FRAGMENT, + ty: wgpu::BindingType::Texture { + sample_type: wgpu::TextureSampleType::Float { filterable: true }, + view_dimension: wgpu::TextureViewDimension::D2, + multisampled: false, + }, + count: None, + }, + wgpu::BindGroupLayoutEntry { + binding: 1, + visibility: wgpu::ShaderStages::FRAGMENT, + ty: wgpu::BindingType::Sampler(wgpu::SamplerBindingType::Filtering), + count: None, + }, + ], + }); + let pipeline_layout = device.create_pipeline_layout(&wgpu::PipelineLayoutDescriptor { + label: Some("andromeda-window-blit-pl"), + bind_group_layouts: &[&bind_group_layout], + push_constant_ranges: &[], + }); + Self { + shader, + sampler, + bind_group_layout, + pipeline_layout, + pipelines: std::collections::HashMap::new(), + } + } + + /// Get or build the blit pipeline for the given destination format. + pub fn pipeline_for_format( + &mut self, + device: &wgpu::Device, + format: wgpu::TextureFormat, + ) -> &wgpu::RenderPipeline { + self.pipelines.entry(format).or_insert_with(|| { + device.create_render_pipeline(&wgpu::RenderPipelineDescriptor { + label: Some("andromeda-window-blit-pipeline"), + layout: Some(&self.pipeline_layout), + vertex: wgpu::VertexState { + module: &self.shader, + entry_point: Some("vs_main"), + buffers: &[], + compilation_options: Default::default(), + }, + fragment: Some(wgpu::FragmentState { + module: &self.shader, + entry_point: Some("fs_main"), + targets: &[Some(wgpu::ColorTargetState { + format, + blend: None, + write_mask: wgpu::ColorWrites::ALL, + })], + compilation_options: Default::default(), + }), + primitive: wgpu::PrimitiveState::default(), + depth_stencil: None, + multisample: wgpu::MultisampleState::default(), + multiview: None, + cache: None, + }) + }) + } +} + +/// A pending window creation waiting for `ApplicationHandler::resumed` to fire. +pub struct PendingCreation { + pub rid: u32, + pub options: CreateWindowOptions, +} + +/// The mutable "app" state passed to `pump_app_events`. +pub struct WindowingApp { + pub windows: HashMap, + pub rid_by_window_id: HashMap, + pub next_rid: u32, + pub pending_creations: Vec, + pub pending_events: VecDeque, + pub modifiers: ModifiersState, + pub pointer: (f64, f64), + pub mouse_buttons: u32, +} + +impl Default for WindowingApp { + fn default() -> Self { + Self { + windows: HashMap::new(), + rid_by_window_id: HashMap::new(), + // Start rids at 1 so 0 can signal "invalid" from TS if needed. + next_rid: 1, + pending_creations: Vec::new(), + pending_events: VecDeque::new(), + modifiers: ModifiersState::empty(), + pointer: (0.0, 0.0), + mouse_buttons: 0, + } + } +} + +impl WindowingApp { + pub fn reserve_rid(&mut self) -> u32 { + let rid = self.next_rid; + self.next_rid += 1; + rid + } + + pub fn get(&self, rid: u32) -> Option<&WindowData> { + self.windows.get(&rid) + } + + pub fn get_mut(&mut self, rid: u32) -> Option<&mut WindowData> { + self.windows.get_mut(&rid) + } + + pub fn remove(&mut self, rid: u32) -> Option { + if let Some(data) = self.windows.remove(&rid) { + let wid = data.window.id(); + self.rid_by_window_id.remove(&wid); + Some(data) + } else { + None + } + } +} + +/// Owns the event loop plus the app state. One instance lives in `OpsStorage`. +#[derive(Default)] +pub struct WindowingState { + pub event_loop: Option>, + pub app: WindowingApp, + pub gpu: Option, +} + +impl WindowingState { + /// Lazily initialize the shared GPU context. + pub fn ensure_gpu(&mut self) -> Result<&WindowingGpu, String> { + if self.gpu.is_some() { + return Ok(self.gpu.as_ref().unwrap()); + } + let instance = wgpu::Instance::new(&wgpu::InstanceDescriptor { + backends: wgpu::Backends::all(), + ..Default::default() + }); + let adapter = + futures::executor::block_on(instance.request_adapter(&wgpu::RequestAdapterOptions { + power_preference: wgpu::PowerPreference::default(), + compatible_surface: None, + force_fallback_adapter: false, + })) + .map_err(|e| format!("failed to request wgpu adapter: {e}"))?; + let (device, queue) = + futures::executor::block_on(adapter.request_device(&wgpu::DeviceDescriptor { + required_features: wgpu::Features::empty(), + required_limits: wgpu::Limits::default(), + label: Some("andromeda-window-device"), + memory_hints: Default::default(), + trace: wgpu::Trace::default(), + experimental_features: wgpu::ExperimentalFeatures::default(), + })) + .map_err(|e| format!("failed to request wgpu device: {e}"))?; + self.gpu = Some(WindowingGpu { + instance, + adapter, + device, + queue, + blit: None, + }); + Ok(self.gpu.as_ref().unwrap()) + } + + /// Lazily initialize the blit pipeline state. + pub fn ensure_blit(&mut self) -> Result<&mut WindowingGpu, String> { + let gpu = self + .gpu + .as_mut() + .ok_or_else(|| "GPU not initialized".to_string())?; + if gpu.blit.is_none() { + gpu.blit = Some(BlitResources::new(&gpu.device)); + } + Ok(gpu) + } +} + +impl WindowingState { + pub fn ensure_event_loop(&mut self) -> Result<&mut EventLoop<()>, String> { + if self.event_loop.is_none() { + let event_loop = + EventLoop::new().map_err(|e| format!("failed to initialize event loop: {e}"))?; + self.event_loop = Some(event_loop); + } + Ok(self.event_loop.as_mut().unwrap()) + } + + pub fn pump(&mut self, timeout: Duration) { + if let Some(event_loop) = self.event_loop.as_mut() { + let _ = event_loop.pump_app_events(Some(timeout), &mut self.app); + } + } +} + +impl ApplicationHandler<()> for WindowingApp { + fn resumed(&mut self, event_loop: &ActiveEventLoop) { + let pending = std::mem::take(&mut self.pending_creations); + for create in pending { + let attrs = WindowAttributes::default() + .with_title(&create.options.title) + .with_inner_size(winit::dpi::LogicalSize::new( + create.options.width, + create.options.height, + )) + .with_resizable(create.options.resizable) + .with_visible(create.options.visible); + match event_loop.create_window(attrs) { + Ok(window) => { + let window_id = window.id(); + window.request_redraw(); + window.focus_window(); + let window = Rc::new(window); + self.windows.insert( + create.rid, + WindowData { + window: window.clone(), + closed: false, + surface: None, + surface_config: None, + }, + ); + self.rid_by_window_id.insert(window_id, create.rid); + } + Err(e) => { + eprintln!( + "[andromeda/window] failed to create window (rid {}): {e}", + create.rid + ); + self.pending_events + .push_back(SerializedWindowEvent::close(create.rid)); + } + } + } + } + + fn window_event( + &mut self, + _event_loop: &ActiveEventLoop, + window_id: WindowId, + event: WindowEvent, + ) { + let Some(&rid) = self.rid_by_window_id.get(&window_id) else { + return; + }; + + match event { + WindowEvent::CloseRequested => { + self.pending_events + .push_back(SerializedWindowEvent::close(rid)); + } + WindowEvent::Resized(size) => { + let scale = self + .windows + .get(&rid) + .map(|w| w.window.scale_factor()) + .unwrap_or(1.0); + self.pending_events.push_back(SerializedWindowEvent::resize( + rid, + size.width, + size.height, + scale, + )); + } + WindowEvent::ModifiersChanged(mods) => { + self.modifiers = mods.state(); + } + WindowEvent::CursorMoved { position, .. } => { + self.pointer = (position.x, position.y); + let detail = self.mouse_detail(-1); + self.pending_events.push_back(SerializedWindowEvent { + rid, + kind: "mousemove", + detail, + }); + } + WindowEvent::MouseInput { state, button, .. } => { + let btn = mouse_button_index(button); + let shift = btn.clamp(0, 31) as u32; + let bit = 1u32 << shift; + match state { + ElementState::Pressed => self.mouse_buttons |= bit, + ElementState::Released => self.mouse_buttons &= !bit, + } + let kind = match state { + ElementState::Pressed => "mousedown", + ElementState::Released => "mouseup", + }; + let detail = self.mouse_detail(btn); + self.pending_events + .push_back(SerializedWindowEvent { rid, kind, detail }); + } + WindowEvent::KeyboardInput { + event: key_event, .. + } => { + let kind = match key_event.state { + ElementState::Pressed => "keydown", + ElementState::Released => "keyup", + }; + let detail = self.key_detail(&key_event); + self.pending_events + .push_back(SerializedWindowEvent { rid, kind, detail }); + } + WindowEvent::RedrawRequested => { + if let Some(data) = self.windows.get(&rid) + && !data.closed + { + data.window.request_redraw(); + } + } + WindowEvent::Destroyed => { + self.windows.remove(&rid); + self.rid_by_window_id.remove(&window_id); + } + _ => {} + } + } +} + +impl WindowingApp { + fn mouse_detail(&self, button: i32) -> EventDetail { + EventDetail::Mouse { + x: self.pointer.0, + y: self.pointer.1, + button, + buttons: self.mouse_buttons, + alt_key: self.modifiers.alt_key(), + ctrl_key: self.modifiers.control_key(), + meta_key: self.modifiers.super_key(), + shift_key: self.modifiers.shift_key(), + } + } + + fn key_detail(&self, ev: &KeyEvent) -> EventDetail { + build_key_detail( + &ev.physical_key, + &ev.logical_key, + ev.location, + ev.repeat, + self.modifiers, + ) + } +} + +fn build_key_detail( + physical_key: &PhysicalKey, + logical_key: &winit::keyboard::Key, + location: winit::keyboard::KeyLocation, + repeat: bool, + modifiers: ModifiersState, +) -> EventDetail { + use super::keymap; + + let (code, key_code) = match physical_key { + PhysicalKey::Code(c) => ( + keymap::physical_key_to_code(*c).to_string(), + keymap::physical_key_to_legacy_keycode(*c), + ), + PhysicalKey::Unidentified(_) => ("Unidentified".to_string(), 0), + }; + let key = match logical_key { + winit::keyboard::Key::Character(s) => s.as_str().to_string(), + winit::keyboard::Key::Named(named) => keymap::named_key_to_spec(named).to_string(), + winit::keyboard::Key::Unidentified(_) => "Unidentified".to_string(), + winit::keyboard::Key::Dead(Some(c)) => c.to_string(), + winit::keyboard::Key::Dead(None) => "Dead".to_string(), + }; + EventDetail::Key { + key, + code, + key_code, + which: key_code, + location: keymap::location_to_u8(location), + alt_key: modifiers.alt_key(), + ctrl_key: modifiers.control_key(), + meta_key: modifiers.super_key(), + shift_key: modifiers.shift_key(), + repeat, + is_composing: false, + } +} + +fn mouse_button_index(button: MouseButton) -> i32 { + match button { + MouseButton::Left => 0, + MouseButton::Middle => 1, + MouseButton::Right => 2, + MouseButton::Back => 3, + MouseButton::Forward => 4, + MouseButton::Other(n) => 5 + n as i32, + } +} diff --git a/crates/runtime/src/recommended.rs b/crates/runtime/src/recommended.rs index 6eaefe6..f0c8460 100644 --- a/crates/runtime/src/recommended.rs +++ b/crates/runtime/src/recommended.rs @@ -2,7 +2,7 @@ // License, v. 2.0. If a copy of the MPL was not distributed with this // file, You can obtain one at https://mozilla.org/MPL/2.0/. -use andromeda_core::{Extension, HostData}; +use andromeda_core::{Extension, HostData, PreTickHook}; use nova_vm::{ ecmascript::{GcAgent, PromiseCapability, RealmRoot, String as NovaString, Value}, engine::{Bindable, Global}, @@ -46,6 +46,8 @@ pub fn recommended_extensions() -> Vec { crate::ServeExt::new_extension(), #[cfg(feature = "canvas")] crate::CanvasExt::new_extension(), + #[cfg(feature = "window")] + crate::WindowExt::new_extension(), #[cfg(feature = "crypto")] crate::CryptoExt::new_extension(), #[cfg(feature = "storage")] @@ -61,6 +63,20 @@ pub fn recommended_builtins() -> Vec<&'static str> { vec![include_str!("../../../namespace/mod.ts")] } +/// Returns the per-iteration runtime hook when any bundled extension needs +/// to drive its own event loop on the main thread. Currently used by the +/// `window` extension to pump winit events; `None` otherwise. +pub fn recommended_pre_tick_hook() -> Option> { + #[cfg(feature = "window")] + { + Some(Box::new(crate::ext::window::pump_windowing_state)) + } + #[cfg(not(feature = "window"))] + { + None + } +} + #[hotpath::measure] pub fn recommended_eventloop_handler( macro_task: RuntimeMacroTask, diff --git a/examples/brickbreaker.ts b/examples/brickbreaker.ts new file mode 100644 index 0000000..a1ab16e --- /dev/null +++ b/examples/brickbreaker.ts @@ -0,0 +1,287 @@ +const WIDTH = 640; +const HEIGHT = 480; + +const PADDLE_W = 90; +const PADDLE_H = 12; +const PADDLE_Y = HEIGHT - 40; +const PADDLE_SPEED = 7; + +const BALL_R = 7; +const BALL_INIT_VX = 4.2; +const BALL_INIT_VY = -4.2; +const BALL_SPEED_UP = 1.03; +const BALL_MAX_SPEED = 9; + +const BRICK_COLS = 10; +const BRICK_ROWS = 5; +const BRICK_W = 56; +const BRICK_H = 18; +const BRICK_GAP = 4; +const BRICK_OFFSET_X = + (WIDTH - (BRICK_COLS * (BRICK_W + BRICK_GAP) - BRICK_GAP)) / 2; +const BRICK_OFFSET_Y = 50; +const ROW_COLORS = ["#ff3355", "#ff9533", "#ffd633", "#33cc66", "#3388ff"]; + +interface Paddle { + x: number; + y: number; + w: number; + h: number; +} +interface Ball { + x: number; + y: number; + vx: number; + vy: number; + r: number; +} +interface Brick { + x: number; + y: number; + w: number; + h: number; + color: string; + alive: boolean; +} +type Phase = "playing" | "game-over" | "you-win"; + +interface GameState { + paddle: Paddle; + ball: Ball; + bricks: Brick[]; + score: number; + phase: Phase; +} + +function buildBricks(): Brick[] { + const out: Brick[] = []; + for (let row = 0; row < BRICK_ROWS; row++) { + for (let col = 0; col < BRICK_COLS; col++) { + out.push({ + x: BRICK_OFFSET_X + col * (BRICK_W + BRICK_GAP), + y: BRICK_OFFSET_Y + row * (BRICK_H + BRICK_GAP), + w: BRICK_W, + h: BRICK_H, + color: ROW_COLORS[row] ?? "#aaaaaa", + alive: true, + }); + } + } + return out; +} + +function initialState(): GameState { + return { + paddle: { + x: (WIDTH - PADDLE_W) / 2, + y: PADDLE_Y, + w: PADDLE_W, + h: PADDLE_H, + }, + ball: { + x: WIDTH / 2, + y: PADDLE_Y - BALL_R - 2, + vx: BALL_INIT_VX, + vy: BALL_INIT_VY, + r: BALL_R, + }, + bricks: buildBricks(), + score: 0, + phase: "playing", + }; +} + +// Closest-point-on-rect distance test — lets ball-vs-brick behave like a +// proper circle/AABB collision (nice corner bounces, no tunneling at modest +// speeds). +function ballIntersectsRect( + ball: Ball, + rx: number, + ry: number, + rw: number, + rh: number, +): boolean { + const cx = Math.max(rx, Math.min(ball.x, rx + rw)); + const cy = Math.max(ry, Math.min(ball.y, ry + rh)); + const dx = ball.x - cx; + const dy = ball.y - cy; + return dx * dx + dy * dy <= ball.r * ball.r; +} + +function clampSpeed(ball: Ball) { + const speed = Math.hypot(ball.vx, ball.vy); + if (speed > BALL_MAX_SPEED) { + const s = BALL_MAX_SPEED / speed; + ball.vx *= s; + ball.vy *= s; + } +} + +const win = Andromeda.createWindow({ + title: "Andromeda Brick Breaker", + width: WIDTH, + height: HEIGHT, +}); +console.log(`window ${win.rid} (${win.rawHandle().system})`); +const canvas = new OffscreenCanvas(WIDTH, HEIGHT); +const ctx = canvas.getContext("2d")!; + +const keys = new Set(); +let state: GameState = initialState(); + +win.addEventListener("keydown", (e: any) => { + const code: string = e.detail.code; + if (code === "Escape") { + win.close(); + return; + } + keys.add(code); + if (code === "Space" && state.phase !== "playing") { + state = initialState(); + } +}); + +win.addEventListener("keyup", (e: any) => { + keys.delete(e.detail.code); +}); + +win.addEventListener("close", () => { + console.log("breakout: window close requested"); +}); + +// ----------------------------------------------------------------------- +// Update + +function update() { + if (state.phase !== "playing") return; + + // Paddle. + const left = keys.has("ArrowLeft") || keys.has("KeyA"); + const right = keys.has("ArrowRight") || keys.has("KeyD"); + if (left) state.paddle.x -= PADDLE_SPEED; + if (right) state.paddle.x += PADDLE_SPEED; + if (state.paddle.x < 0) state.paddle.x = 0; + if (state.paddle.x + state.paddle.w > WIDTH) + state.paddle.x = WIDTH - state.paddle.w; + + // Ball. + const b = state.ball; + b.x += b.vx; + b.y += b.vy; + + // Walls. + if (b.x - b.r < 0) { + b.x = b.r; + b.vx = Math.abs(b.vx); + } else if (b.x + b.r > WIDTH) { + b.x = WIDTH - b.r; + b.vx = -Math.abs(b.vx); + } + if (b.y - b.r < 0) { + b.y = b.r; + b.vy = Math.abs(b.vy); + } + + // Paddle collision — only when the ball is moving down, so a jittery + // paddle on the underside doesn't re-trap the ball. + if ( + b.vy > 0 && + ballIntersectsRect( + b, + state.paddle.x, + state.paddle.y, + state.paddle.w, + state.paddle.h, + ) + ) { + b.y = state.paddle.y - b.r; + b.vy = -Math.abs(b.vy); + const rawHit = + (b.x - (state.paddle.x + state.paddle.w / 2)) / (state.paddle.w / 2); + const hit = Math.max(-1, Math.min(1, rawHit)); + const speed = Math.hypot(b.vx, b.vy); + b.vx = hit * speed * 0.85; + b.vy = -Math.sqrt(Math.max(0.001, speed * speed - b.vx * b.vx)); + } + + for (const brick of state.bricks) { + if (!brick.alive) continue; + if (!ballIntersectsRect(b, brick.x, brick.y, brick.w, brick.h)) continue; + brick.alive = false; + state.score += 10; + const overlapX = Math.min( + Math.abs(b.x + b.r - brick.x), + Math.abs(brick.x + brick.w - (b.x - b.r)), + ); + const overlapY = Math.min( + Math.abs(b.y + b.r - brick.y), + Math.abs(brick.y + brick.h - (b.y - b.r)), + ); + if (overlapX < overlapY) { + b.vx = -b.vx; + } else { + b.vy = -b.vy; + } + b.vx *= BALL_SPEED_UP; + b.vy *= BALL_SPEED_UP; + clampSpeed(b); + break; + } + + if (state.bricks.every((br) => !br.alive)) { + state.phase = "you-win"; + } else if (b.y - b.r > HEIGHT) { + state.phase = "game-over"; + } +} + +function render() { + ctx.fillStyle = "#111827"; + ctx.fillRect(0, 0, WIDTH, HEIGHT); + + for (const brick of state.bricks) { + if (!brick.alive) continue; + ctx.fillStyle = brick.color; + ctx.fillRect(brick.x, brick.y, brick.w, brick.h); + } + + ctx.fillStyle = "#f3f4f6"; + ctx.fillRect(state.paddle.x, state.paddle.y, state.paddle.w, state.paddle.h); + + ctx.fillStyle = "#fbbf24"; + ctx.beginPath(); + ctx.arc(state.ball.x, state.ball.y, state.ball.r, 0, Math.PI * 2); + ctx.fill(); + + ctx.fillStyle = "#e5e7eb"; + ctx.font = "18px sans-serif"; + ctx.textAlign = "left"; + ctx.fillText(`Score: ${state.score}`, 12, 28); + + if (state.phase !== "playing") { + ctx.fillStyle = "rgba(0, 0, 0, 0.7)"; + ctx.fillRect(0, HEIGHT / 2 - 60, WIDTH, 120); + ctx.fillStyle = "#ffffff"; + ctx.font = "36px sans-serif"; + ctx.textAlign = "center"; + const heading = state.phase === "you-win" ? "You Win!" : "Game Over"; + ctx.fillText(heading, WIDTH / 2, HEIGHT / 2 - 10); + ctx.font = "18px sans-serif"; + ctx.fillText("press Space to restart", WIDTH / 2, HEIGHT / 2 + 28); + } +} + +let frameCount = 0; +const fpsStart = Date.now(); +await Andromeda.Window.mainloop(() => { + frameCount++; + update(); + render(); + win.presentCanvas(canvas); + if (frameCount % 120 === 0) { + const elapsed = (Date.now() - fpsStart) / 1000; + console.log( + `breakout: frame ${frameCount} — avg fps ${(frameCount / elapsed).toFixed(1)}`, + ); + } +}); diff --git a/examples/window.ts b/examples/window.ts new file mode 100644 index 0000000..d4c07ea --- /dev/null +++ b/examples/window.ts @@ -0,0 +1,63 @@ +const WIDTH = 640; +const HEIGHT = 480; + +const win = Andromeda.createWindow({ + title: "Andromeda Canvas Window", + width: WIDTH, + height: HEIGHT, +}); +console.log("opened window", win.rid, win.rawHandle().system); + +const canvas = new OffscreenCanvas(WIDTH, HEIGHT); +const ctx = canvas.getContext("2d")!; + +win.addEventListener("resize", (e: any) => { + console.log( + `resize → ${e.detail.width}x${e.detail.height} @${e.detail.scaleFactor}x`, + ); +}); + +win.addEventListener("keydown", (e: any) => { + if (e.detail.code === "Escape") { + console.log("escape pressed, closing"); + win.close(); + } +}); + +win.addEventListener("close", () => { + console.log("window close requested"); +}); + +let frame = 0; +await Andromeda.Window.mainloop(() => { + frame++; + const t = frame / 60; + + ctx.fillStyle = `rgb(${(20 + 20 * Math.sin(t)) | 0}, ${(20 + 20 * Math.sin(t + 1)) | 0}, 40)`; + ctx.fillRect(0, 0, WIDTH, HEIGHT); + + for (let i = 0; i < 3; i++) { + const cx = WIDTH * (0.25 + i * 0.25); + const cy = HEIGHT * 0.5 + Math.sin(t + i) * 60; + const size = 80 + Math.sin(t * 2 + i) * 20; + const r = 128 + Math.sin(t + i * 2) * 127; + const g = 128 + Math.sin(t + i * 2 + 2) * 127; + const b = 128 + Math.sin(t + i * 2 + 4) * 127; + ctx.fillStyle = `rgb(${r | 0}, ${g | 0}, ${b | 0})`; + ctx.fillRect(cx - size / 2, cy - size / 2, size, size); + } + + try { + win.presentCanvas(canvas); + } catch (e) { + console.log("present err:", String(e)); + win.close(); + return; + } + + if (frame === 1 || frame % 60 === 0) { + console.log(`tick ${frame}`); + } +}); + +console.log("mainloop exited cleanly"); diff --git a/namespace/mod.ts b/namespace/mod.ts index 42adc4f..799358e 100644 --- a/namespace/mod.ts +++ b/namespace/mod.ts @@ -914,6 +914,65 @@ const Andromeda = { } return httpServe; }, + + /** + * Native window class backed by winit. Only available when Andromeda is + * built with the `window` feature enabled. Extends `EventTarget`; events + * `close`, `resize`, `keydown`, `keyup`, `mousemove`, `mousedown`, + * `mouseup` are dispatched during `Andromeda.Window.mainloop`. + * + * @example + * ```ts + * const w = new Andromeda.Window({ title: "Hi", width: 640, height: 480 }); + * w.addEventListener("close", () => console.log("bye")); + * await Andromeda.Window.mainloop(() => { + * // render this frame + * }); + * ``` + */ + get Window(): any { + // @ts-ignore - cross-file handoff from ext/window/mod.ts + const Klass = globalThis.__andromeda_window_class; + if (typeof Klass !== "function") { + throw new Error( + "Window extension is not available. Make sure the 'window' feature is enabled.", + ); + } + // @ts-ignore - cross-file handoff from ext/window/mod.ts + const mainloop = globalThis.__andromeda_window_mainloop; + if (mainloop && !Klass.mainloop) { + Object.defineProperty(Klass, "mainloop", { + value: mainloop, + enumerable: true, + }); + } + return Klass; + }, + + /** + * Shorthand constructor for `new Andromeda.Window(options)`. + * + * @example + * ```ts + * const win = Andromeda.createWindow({ title: "Hello", width: 320, height: 240 }); + * ``` + */ + createWindow(options: { + title?: string; + width?: number; + height?: number; + resizable?: boolean; + visible?: boolean; + } = {}): any { + // @ts-ignore - cross-file handoff from ext/window/mod.ts + const Klass = globalThis.__andromeda_window_class; + if (typeof Klass !== "function") { + throw new Error( + "Window extension is not available. Make sure the 'window' feature is enabled.", + ); + } + return new Klass(options); + }, }; /** diff --git a/types/internals.d.ts b/types/internals.d.ts index bb4f371..9a59c05 100644 --- a/types/internals.d.ts +++ b/types/internals.d.ts @@ -2075,4 +2075,67 @@ declare namespace __andromeda__ { * @returns Promise resolving to JSON string with {success, code, signal, stdout, stderr}. */ export function internal_command_child_output(rid: string): Promise; + + // --------------------------------------------------------------------- + // Window (optional, feature = "window") + // --------------------------------------------------------------------- + + /** + * Create a new native window. + * @param optionsJson JSON-serialized {@link AndromedaWindowOptions}. + * @returns String-form rid of the new window. + */ + export function internal_window_create(optionsJson: string): string; + + /** Close a window by rid. Idempotent. */ + export function internal_window_close(rid: number): void; + + /** + * Pump pending winit events and return them as a JSON array of + * `{ rid, type, detail }` records. + */ + export function internal_window_poll_events(): string; + + /** + * Return the raw window + display handle for the given rid as a JSON + * string. Pointer-sized values are strings to survive JS number precision. + */ + export function internal_window_raw_handle(rid: number): string; + + export function internal_window_set_title(rid: number, title: string): void; + + /** Returns a JSON string `{"width":N,"height":N,"scaleFactor":N}`. */ + export function internal_window_get_size(rid: number): string; + + export function internal_window_set_size(rid: number, width: number, height: number): void; + + export function internal_window_set_visible(rid: number, visible: boolean): void; + + /** Clear the window's swapchain to RGBA (0..1 channels) and present one frame. */ + export function internal_window_present_color( + rid: number, + r: number, + g: number, + b: number, + a: number, + ): void; + + /** + * Flush the canvas's pending commands and blit its latest frame into + * the window's swapchain. Present after. Only registered when both + * `window` and `canvas` features are compiled in. + */ + export function internal_window_present_canvas( + window_rid: number, + canvas_rid: number, + ): void; +} + +/** Options accepted by {@link Andromeda.createWindow}. */ +interface AndromedaWindowOptions { + title?: string; + width?: number; + height?: number; + resizable?: boolean; + visible?: boolean; } diff --git a/types/window.d.ts b/types/window.d.ts new file mode 100644 index 0000000..09afbeb --- /dev/null +++ b/types/window.d.ts @@ -0,0 +1,188 @@ +// This Source Code Form is subject to the terms of the Mozilla Public +// License, v. 2.0. If a copy of the MPL was not distributed with this +// file, You can obtain one at https://mozilla.org/MPL/2.0/. + +/** + * Andromeda Window API — inspired by `deno-windowing/dwm`, backed by winit. + */ +export interface CreateWindowOptions { + /** OS window title. Defaults to `"Andromeda"`. */ + title?: string; + /** Inner width in logical pixels. Defaults to `800`. */ + width?: number; + /** Inner height in logical pixels. Defaults to `600`. */ + height?: number; + /** Whether the user can resize the window. Defaults to `true`. */ + resizable?: boolean; + /** Whether the window is visible on creation. Defaults to `true`. */ + visible?: boolean; +} + +export interface ResizeEventDetail { + width: number; + height: number; + scaleFactor: number; +} + +/** + * Detail payload for `keydown` / `keyup` events on an `Andromeda.Window`. + * + * Values match the web platform's `KeyboardEvent` contract so code written + * against the browser's event object carries over 1:1 (modulo the plain- + * object wrapper — see {@link WindowEvent}). + */ +export interface KeyEventDetail { + /** + * The `key` attribute value per the UI Events spec. + * https://developer.mozilla.org/en-US/docs/Web/API/KeyboardEvent/key + */ + key: string; + + /** + * The `code` attribute — physical key identifier independent of layout. + * https://developer.mozilla.org/en-US/docs/Web/API/KeyboardEvent/code + */ + code: string; + + /** + * Deprecated legacy numeric code per the MDN keyCode table. + * https://developer.mozilla.org/en-US/docs/Web/API/KeyboardEvent/keyCode + */ + keyCode: number; + + /** + * Alias of {@link keyCode} — always equal. Present only because legacy + * code sometimes reads `event.which` instead. + */ + which: number; + + /** + * Physical key location. + * https://developer.mozilla.org/en-US/docs/Web/API/KeyboardEvent/location + */ + location: 0 | 1 | 2 | 3; + + /** True when the Alt / Option modifier is held. */ + altKey: boolean; + /** True when the Control modifier is held. */ + ctrlKey: boolean; + /** True when the Meta / Command / Super modifier is held. */ + metaKey: boolean; + /** True when the Shift modifier is held. */ + shiftKey: boolean; + + /** True when the event is a held-key auto-repeat. */ + repeat: boolean; + + /** + * Whether the event is part of an IME composition session. + * https://developer.mozilla.org/en-US/docs/Web/API/KeyboardEvent/isComposing + */ + isComposing: boolean; +} + +export interface MouseEventDetail { + x: number; + y: number; + /** DOM-aligned button index: 0=left, 1=middle, 2=right, 3/4=back/forward, -1 on mousemove. */ + button: number; + /** Bitmask of currently pressed buttons. */ + buttons: number; + altKey: boolean; + ctrlKey: boolean; + metaKey: boolean; + shiftKey: boolean; +} + +/** + * A native OS window. + */ +export interface RawWindowHandleData { + /** Native windowing system for this handle. */ + system: "cocoa" | "win32" | "x11" | "wayland"; + /** Pointer-sized window handle, encoded as a string (convert with `BigInt`). */ + windowHandle: string; + /** Pointer-sized display handle, encoded as a string (`"0"` when not applicable). */ + displayHandle: string; + width: number; + height: number; +} + +/** Plain-object event dispatched to window listeners.*/ +export interface WindowEvent { + type: string; + detail: D; + target: Window; +} + +export type WindowEventListener = (event: WindowEvent) => void; + +export declare class Window { + constructor(options?: CreateWindowOptions); + readonly rid: number; + readonly title: string; + readonly width: number; + readonly height: number; + readonly closed: boolean; + close(): void; + + addEventListener(type: string, listener: WindowEventListener): void; + removeEventListener(type: string, listener: WindowEventListener): void; + dispatchEvent(event: WindowEvent): boolean; + + /** + * Return the native window + display handles and current size. Shape- + * compatible with `Deno.UnsafeWindowSurface` so WebGPU-surface bridges can + * pass the object through verbatim. + */ + rawHandle(): RawWindowHandleData; + + setTitle(title: string): void; + getSize(): { width: number; height: number; scaleFactor: number }; + setSize(width: number, height: number): void; + setVisible(visible: boolean): void; + + /** + * Clear the window's swapchain to an RGBA color and present a frame. + * Channel values are 0..1. Useful as a smoke test of the render loop + * before wiring richer canvas/WebGPU surface integrations. + */ + present(r: number, g: number, b: number, a?: number): void; + + /** + * Present the latest frame of an `OffscreenCanvas` into this window. + * Flushes any pending canvas 2D draw commands and then blits the + * rendered texture to the swapchain via a scaled fullscreen pass. The + * canvas may be any size; it will stretch to fill the window. + * + * Requires the runtime to be built with both the `window` and `canvas` + * features enabled; throws a clear error otherwise. + */ + presentCanvas(canvas: OffscreenCanvas): void; + + /** + * Drive the winit event loop, dispatching events to all open windows. + * Returns once every open window has been closed. + * + * @param callback Optional per-frame callback invoked after events are + * dispatched. Useful for rendering or state updates. + */ + static mainloop(callback?: () => void | Promise): Promise; +} + +/** Shorthand for `new Andromeda.Window(options)`. */ +export declare function createWindow(options?: CreateWindowOptions): Window; + +declare global { + namespace Andromeda { + export { + createWindow, + CreateWindowOptions, + KeyEventDetail, + MouseEventDetail, + RawWindowHandleData, + ResizeEventDetail, + Window, + }; + } +}